1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 4 5 // me@arsd:~/.kde/share/config$ vim kdeglobals 6 7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 8 9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 10 11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 12 13 // responsive minigui, menu search, and file open with a preview hook on the side. 14 15 // FIXME: add menu checkbox and menu icon eventually 16 17 /* 18 19 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan 20 21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 22 */ 23 24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 25 26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 27 28 // FIXME: opt-in file picker widget with image support 29 30 // FIXME: number widget 31 32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 34 35 // osx style menu search. 36 37 // would be cool for a scroll bar to have marking capabilities 38 // kinda like vim's marks just on clicks etc and visual representation 39 // generically. may be cool to add an up arrow to the bottom too 40 // 41 // leave a shadow of where you last were for going back easily 42 43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 45 // the window. 46 47 // so what about context menus? 48 49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 50 51 // FIXME: make the scroll thing go to bottom when the content changes. 52 53 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image 54 55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 56 57 58 // FIXME: add a command search thingy built in and implement tip. 59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 60 61 // On Windows: 62 // FIXME: various labels look broken in high contrast mode 63 // FIXME: changing themes while the program is upen doesn't trigger a redraw 64 65 // add note about manifest to documentation. also icons. 66 67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 68 // FIXME: clear the corner of scrollbars if they pop up 69 70 // minigui needs to have a stdout redirection for gui mode on windows writeln 71 72 // I kinda wanna do state reacting. sort of. idk tho 73 74 // need a viewer widget that works like a web page - arrows scroll down consistently 75 76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 77 78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 79 // and help info about menu items. 80 // and search in menus? 81 82 // FIXME: a scroll area event signaling when a thing comes into view might be good 83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 84 85 // FIXME: unify Windows style line endings 86 87 /* 88 TODO: 89 90 pie menu 91 92 class Form with submit behavior -- see AutomaticDialog 93 94 disabled widgets and menu items 95 96 event cleanup 97 tooltips. 98 api improvements 99 100 margins are kinda broken, they don't collapse like they should. at least. 101 102 a table form btw would be a horizontal layout of vertical layouts holding each column 103 that would give the same width things 104 */ 105 106 /* 107 108 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets 109 */ 110 111 /++ 112 minigui is a smallish GUI widget library, aiming to be on par with at least 113 HTML4 forms and a few other expected gui components. It uses native controls 114 on Windows and does its own thing on Linux (Mac is not currently supported but 115 may be later, and should use native controls) to keep size down. The Linux 116 appearance is similar to Windows 95 and avoids using images to maintain network 117 efficiency on remote X connections, though you can customize that. 118 119 120 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 121 on which it is built. simpledisplay provides the low-level interfaces and minigui 122 builds the concept of widgets inside the windows on top of it. 123 124 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 125 It isn't hugely concerned with appearance - on Windows, it just uses the native 126 controls and native theme, and on Linux, it keeps it simple and I may change that 127 at any time, though after May 2021, you can customize some things with css-inspired 128 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 129 you can use the custom implementation there too, but... you shouldn't.) 130 131 The event model is similar to what you use in the browser with Javascript and the 132 layout engine tries to automatically fit things in, similar to a css flexbox. 133 134 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 135 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 136 console and other visual bugs. 137 138 HTML_To_Classes: 139 $(SMALL_TABLE 140 HTML Code | Minigui Class 141 142 `<input type="text">` | [LineEdit] 143 `<textarea>` | [TextEdit] 144 `<select>` | [DropDownSelection] 145 `<input type="checkbox">` | [Checkbox] 146 `<input type="radio">` | [Radiobox] 147 `<button>` | [Button] 148 ) 149 150 151 Stretchiness: 152 The default is 4. You can use larger numbers for things that should 153 consume a lot of space, and lower numbers for ones that are better at 154 smaller sizes. 155 156 Overlapped_input: 157 COMING EVENTUALLY: 158 minigui will include a little bit of I/O functionality that just works 159 with the event loop. If you want to get fancy, I suggest spinning up 160 another thread and posting events back and forth. 161 162 $(H2 Add ons) 163 See the `minigui_addons` directory in the arsd repo for some add on widgets 164 you can import separately too. 165 166 $(H3 XML definitions) 167 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 168 169 $(H3 Scriptability) 170 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 171 in this documentation, it means you can call it from the script language. 172 173 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 174 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 175 176 --- 177 import arsd.minigui_xml; 178 import arsd.script; 179 180 var globals = var.emptyObject; 181 globals.makeWidgetFromString = &makeWidgetFromString; 182 183 // this now works 184 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 185 --- 186 187 More to come. 188 189 History: 190 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 191 192 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 193 tag this as version 2.0. 194 195 Among the changes: 196 $(LIST 197 * The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard. 198 199 See [Event] for details. 200 201 * A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead. 202 203 See [DoubleClickEvent] for details. 204 205 * Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows. 206 207 See [Widget.Style] for details. 208 209 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 210 211 * Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it. 212 213 * Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst. 214 215 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 216 217 * Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does. 218 219 * Various non-breaking additions. 220 ) 221 +/ 222 module arsd.minigui; 223 224 /++ 225 This hello world sample will have an oversized button, but that's ok, you see your first window! 226 +/ 227 version(Demo) 228 unittest { 229 import arsd.minigui; 230 231 void main() { 232 auto window = new MainWindow(); 233 234 // note the parent widget is almost always passed as the last argument to a constructor 235 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 236 auto button = new Button("Close", window); 237 button.addWhenTriggered({ 238 window.close(); 239 }); 240 241 window.loop(); 242 } 243 244 main(); // exclude from docs 245 } 246 247 /++ 248 This example shows one way you can partition your window into a header 249 and sidebar. Here, the header and sidebar have a fixed width, while the 250 rest of the content sizes with the window. 251 252 It might be a new way of thinking about window layout to do things this 253 way - perhaps [GridLayout] more matches your style of thought - but the 254 concept here is to partition the window into sub-boxes with a particular 255 size, then partition those boxes into further boxes. 256 257 $(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.) 258 259 So to make the header, start with a child layout that has a max height. 260 It will use that space from the top, then the remaining children will 261 split the remaining area, meaning you can think of is as just being another 262 box you can split again. Keep splitting until you have the look you desire. 263 +/ 264 // https://github.com/adamdruppe/arsd/issues/310 265 version(minigui_screenshots) 266 @Screenshot("layout") 267 unittest { 268 import arsd.minigui; 269 270 // This helper class is just to help make the layout boxes visible. 271 // think of it like a <div style="background-color: whatever;"></div> in HTML. 272 class ColorWidget : Widget { 273 this(Color color, Widget parent) { 274 this.color = color; 275 super(parent); 276 } 277 Color color; 278 class Style : Widget.Style { 279 override WidgetBackground background() { return WidgetBackground(color); } 280 } 281 mixin OverrideStyle!Style; 282 } 283 284 void main() { 285 auto window = new Window; 286 287 // the key is to give it a max height. This is one way to do it: 288 auto header = new class HorizontalLayout { 289 this() { super(window); } 290 override int maxHeight() { return 50; } 291 }; 292 // this next line is a shortcut way of doing it too, but it only works 293 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 294 // is good to know how to make a new class like above anyway. 295 // auto header = new HorizontalLayout(50, window); 296 297 auto bar = new HorizontalLayout(window); 298 299 // or since this is so common, VerticalLayout and HorizontalLayout both 300 // can just take an argument in their constructor for max width/height respectively 301 302 // (could have tone this above too, but I wanted to demo both techniques) 303 auto left = new VerticalLayout(100, bar); 304 305 // and this is the main section's container. A plain Widget instance is good enough here. 306 auto container = new Widget(bar); 307 308 // and these just add color to the containers we made above for the screenshot. 309 // in a real application, you can just add your actual controls instead of these. 310 auto headerColorBox = new ColorWidget(Color.teal, header); 311 auto leftColorBox = new ColorWidget(Color.green, left); 312 auto rightColorBox = new ColorWidget(Color.purple, container); 313 314 window.loop(); 315 } 316 317 main(); // exclude from docs 318 } 319 320 321 import arsd.core; 322 public import arsd.simpledisplay; 323 /++ 324 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 325 326 History: 327 Was private until May 15, 2021. 328 +/ 329 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 330 331 version(Windows) { 332 import core.sys.windows.winnls; 333 import core.sys.windows.windef; 334 import core.sys.windows.basetyps; 335 import core.sys.windows.winbase; 336 import core.sys.windows.winuser; 337 import core.sys.windows.wingdi; 338 static import gdi = core.sys.windows.wingdi; 339 } 340 341 version(Windows) { 342 version(minigui_manifest) {} else version=minigui_no_manifest; 343 344 version(minigui_no_manifest) {} else 345 static if(__VERSION__ >= 2_083) 346 version(CRuntime_Microsoft) { // FIXME: mingw? 347 // assume we want commctrl6 whenever possible since there's really no reason not to 348 // and this avoids some of the manifest hassle 349 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 350 } 351 } 352 353 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 354 private bool lastDefaultPrevented; 355 356 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 357 alias scriptable = arsd_jsvar_compatible; 358 359 version(Windows) { 360 // use native widgets when available unless specifically asked otherwise 361 version(custom_widgets) { 362 enum bool UsingCustomWidgets = true; 363 enum bool UsingWin32Widgets = false; 364 } else { 365 version = win32_widgets; 366 enum bool UsingCustomWidgets = false; 367 enum bool UsingWin32Widgets = true; 368 369 // give access to my text system for the rich text cross platform stuff 370 version = use_new_text_system; 371 import arsd.textlayouter; 372 } 373 // and native theming when needed 374 //version = win32_theming; 375 } else { 376 enum bool UsingCustomWidgets = true; 377 enum bool UsingWin32Widgets = false; 378 version=custom_widgets; 379 } 380 381 382 383 /* 384 385 The main goals of minigui.d are to: 386 1) Provide basic widgets that just work in a lightweight lib. 387 I basically want things comparable to a plain HTML form, 388 plus the easy and obvious things you expect from Windows 389 apps like a menu. 390 2) Use native things when possible for best functionality with 391 least library weight. 392 3) Give building blocks to provide easy extension for your 393 custom widgets, or hooking into additional native widgets 394 I didn't wrap. 395 4) Provide interfaces for easy interaction between third 396 party minigui extensions. (event model, perhaps 397 signals/slots, drop-in ease of use bits.) 398 5) Zero non-system dependencies, including Phobos as much as 399 I reasonably can. It must only import arsd.color and 400 my simpledisplay.d. If you need more, it will have to be 401 an extension module. 402 6) An easy layout system that generally works. 403 404 A stretch goal is to make it easy to make gui forms with code, 405 some kind of resource file (xml?) and even a wysiwyg designer. 406 407 Another stretch goal is to make it easy to hook data into the gui, 408 including from reflection. So like auto-generate a form from a 409 function signature or struct definition, or show a list from an 410 array that automatically updates as the array is changed. Then, 411 your program focuses on the data more than the gui interaction. 412 413 414 415 STILL NEEDED: 416 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 417 * slider 418 * listbox 419 * spinner 420 * label? 421 * rich text 422 */ 423 424 425 /+ 426 enum LayoutMethods { 427 verticalFlex, 428 horizontalFlex, 429 inlineBlock, // left to right, no stretch, goes to next line as needed 430 static, // just set to x, y 431 verticalNoStretch, // browser style default 432 433 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 434 435 grid, // magic 436 } 437 +/ 438 439 /++ 440 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. 441 442 443 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. 444 445 --- 446 class MinimalWidget : Widget { 447 this(Widget parent) { 448 super(parent); 449 } 450 } 451 --- 452 453 $(SIDEBAR 454 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. 455 ) 456 457 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. 458 459 Among the things you'll most likely want to change in your custom widget: 460 461 $(LIST 462 * 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.) 463 464 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. 465 466 Do this $(I after) calling the `super` constructor. 467 468 * 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. 469 470 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 471 472 * 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. 473 474 * 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. 475 ) 476 477 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. 478 479 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 480 481 Your own custom-drawn and native system controls can exist side-by-side. 482 483 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. 484 +/ 485 class Widget : ReflectableProperties { 486 487 private bool willDraw() { 488 return true; 489 } 490 491 /+ 492 /++ 493 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 494 495 History: 496 Added September 15, 2021 497 implemented.... ??? 498 +/ 499 void prepareReflection(this This)() { 500 501 } 502 +/ 503 504 private bool _enabled = true; 505 506 /++ 507 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. 508 509 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. 510 511 History: 512 Added November 23, 2021 (dub v10.4) 513 514 Warning: the specific behavior of disabling with parents may change in the future. 515 Bugs: 516 Currently only implemented for widgets backed by native Windows controls. 517 518 See_Also: [disabledReason], [disabledBy] 519 +/ 520 @property bool enabled() { 521 return disabledBy() is null; 522 } 523 524 /// ditto 525 @property void enabled(bool yes) { 526 _enabled = yes; 527 version(win32_widgets) { 528 if(hwnd) 529 EnableWindow(hwnd, yes); 530 } 531 setDynamicState(DynamicState.disabled, yes); 532 } 533 534 private string disabledReason_; 535 536 /++ 537 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. 538 539 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 540 541 History: 542 Added November 23, 2021 (dub v10.4) 543 See_Also: [enabled], [disabledBy] 544 +/ 545 @property string disabledReason() { 546 auto w = disabledBy(); 547 return (w is null) ? null : w.disabledReason_; 548 } 549 550 /// ditto 551 @property void disabledReason(string reason) { 552 disabledReason_ = reason; 553 } 554 555 /++ 556 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. 557 558 History: 559 Added November 25, 2021 (dub v10.4) 560 See_Also: [enabled], [disabledReason] 561 +/ 562 Widget disabledBy() { 563 Widget p = this; 564 while(p) { 565 if(!p._enabled) 566 return p; 567 p = p.parent; 568 } 569 return null; 570 } 571 572 /// Implementations of [ReflectableProperties] interface. See the interface for details. 573 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 574 if(valueIsJson) 575 return SetPropertyResult.wrongFormat; 576 switch(name) { 577 case "name": 578 this.name = value.idup; 579 return SetPropertyResult.success; 580 case "statusTip": 581 this.statusTip = value.idup; 582 return SetPropertyResult.success; 583 default: 584 return SetPropertyResult.noSuchProperty; 585 } 586 } 587 /// ditto 588 void getPropertiesList(scope void delegate(string name) sink) const { 589 sink("name"); 590 sink("statusTip"); 591 } 592 /// ditto 593 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 594 switch(name) { 595 case "name": 596 sink(name, this.name, false); 597 return; 598 case "statusTip": 599 sink(name, this.statusTip, false); 600 return; 601 default: 602 sink(name, null, true); 603 } 604 } 605 606 /++ 607 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 608 609 History: 610 Added November 25, 2021 (dub v10.5) 611 `Point` overload added January 12, 2022 (dub v10.6) 612 +/ 613 int scaleWithDpi(int value, int assumedDpi = 96) { 614 // avoid potential overflow with common special values 615 if(value == int.max) 616 return int.max; 617 if(value == int.min) 618 return int.min; 619 if(value == 0) 620 return 0; 621 return value * currentDpi(assumedDpi) / assumedDpi; 622 } 623 624 /// ditto 625 Point scaleWithDpi(Point value, int assumedDpi = 96) { 626 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 627 } 628 629 /++ 630 Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor. 631 632 Not entirely stable. 633 634 History: 635 Added August 25, 2023 (dub v11.1) 636 +/ 637 final int currentDpi(int assumedDpi = 96) { 638 // assert(parentWindow !is null); 639 // assert(parentWindow.win !is null); 640 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 641 //divide = 138; // to test 1.5x 642 // 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. 643 // this also covers the case when actualDpi returns 0. 644 if(divide < 96) 645 divide = 96; 646 return divide; 647 } 648 649 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 650 // I'll think up something better eventually 651 652 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 653 protected final int defaultLineHeight() { 654 auto cs = getComputedStyle(); 655 if(cs.font && !cs.font.isNull) 656 return cs.font.height() * 5 / 4; 657 else 658 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 659 } 660 661 /++ 662 663 History: 664 Added August 25, 2023 (dub v11.1) 665 +/ 666 protected final int defaultTextHeight(int numberOfLines = 1) { 667 auto cs = getComputedStyle(); 668 if(cs.font && !cs.font.isNull) 669 return cs.font.height() * numberOfLines; 670 else 671 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 672 } 673 674 protected final int defaultTextWidth(const(char)[] text) { 675 auto cs = getComputedStyle(); 676 if(cs.font && !cs.font.isNull) 677 return cs.font.stringWidth(text); 678 else 679 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 680 } 681 682 /++ 683 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. 684 685 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. 686 687 History: 688 Added May 22, 2021 689 +/ 690 protected bool encapsulatedChildren() { 691 return false; 692 } 693 694 private void privateDpiChanged() { 695 dpiChanged(); 696 foreach(child; children) 697 child.privateDpiChanged(); 698 } 699 700 /++ 701 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 702 703 History: 704 Added January 12, 2022 (dub v10.6) 705 +/ 706 protected void dpiChanged() { 707 708 } 709 710 // Default layout properties { 711 712 int minWidth() { return 0; } 713 int minHeight() { 714 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 715 int sum = this.paddingTop + this.paddingBottom; 716 foreach(child; children) { 717 if(child.hidden) 718 continue; 719 sum += child.minHeight(); 720 sum += child.marginTop(); 721 sum += child.marginBottom(); 722 } 723 724 return sum; 725 } 726 int maxWidth() { return int.max; } 727 int maxHeight() { return int.max; } 728 int widthStretchiness() { return 4; } 729 int heightStretchiness() { return 4; } 730 731 /++ 732 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 733 734 History: 735 Added June 15, 2021 (dub v10.1) 736 +/ 737 int widthShrinkiness() { return 0; } 738 /// ditto 739 int heightShrinkiness() { return 0; } 740 741 /++ 742 The initial size of the widget for layout calculations. Default is 0. 743 744 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 745 746 History: 747 Added June 15, 2021 (dub v10.1) 748 +/ 749 int flexBasisWidth() { return 0; } 750 /// ditto 751 int flexBasisHeight() { return 0; } 752 753 /++ 754 Not stable. 755 756 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 757 758 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 759 760 History: 761 Added January 5, 2023 762 +/ 763 Rectangle defaultMargin; 764 /// ditto 765 Rectangle defaultPadding; 766 767 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 768 int marginRight() { return scaleWithDpi(defaultMargin.right); } 769 int marginTop() { return scaleWithDpi(defaultMargin.top); } 770 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 771 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 772 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 773 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 774 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 775 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 776 777 private bool recomputeChildLayoutRequired = true; 778 private static class RecomputeEvent {} 779 private __gshared rce = new RecomputeEvent(); 780 protected final void queueRecomputeChildLayout() { 781 recomputeChildLayoutRequired = true; 782 783 if(this.parentWindow) { 784 auto sw = this.parentWindow.win; 785 assert(sw !is null); 786 if(!sw.eventQueued!RecomputeEvent) { 787 sw.postEvent(rce); 788 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 789 } 790 } 791 792 } 793 794 protected final void recomputeChildLayoutEntry() { 795 if(recomputeChildLayoutRequired) { 796 recomputeChildLayout(); 797 recomputeChildLayoutRequired = false; 798 redraw(); 799 } else { 800 // I still need to check the tree just in case one of them was queued up 801 // and the event came up here instead of there. 802 foreach(child; children) 803 child.recomputeChildLayoutEntry(); 804 } 805 } 806 807 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 808 void recomputeChildLayout() { 809 .recomputeChildLayout!"height"(this); 810 } 811 812 // } 813 814 815 /++ 816 Returns the style's tag name string this object uses. 817 818 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. 819 820 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 821 822 History: 823 Added May 10, 2021 824 +/ 825 string styleTagName() const { 826 string n = typeid(this).name; 827 foreach_reverse(idx, ch; n) 828 if(ch == '.') { 829 n = n[idx + 1 .. $]; 830 break; 831 } 832 return n; 833 } 834 835 /// API for the [styleClassList] 836 static struct ClassList { 837 private Widget widget; 838 839 /// 840 void add(string s) { 841 widget.styleClassList_ ~= s; 842 } 843 844 /// 845 void remove(string s) { 846 foreach(idx, s1; widget.styleClassList_) 847 if(s1 == s) { 848 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 849 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 850 widget.styleClassList_.assumeSafeAppend(); 851 return; 852 } 853 } 854 855 /// Returns true if it was added, false if it was removed. 856 bool toggle(string s) { 857 if(contains(s)) { 858 remove(s); 859 return false; 860 } else { 861 add(s); 862 return true; 863 } 864 } 865 866 /// 867 bool contains(string s) const { 868 foreach(s1; widget.styleClassList_) 869 if(s1 == s) 870 return true; 871 return false; 872 873 } 874 } 875 876 private string[] styleClassList_; 877 878 /++ 879 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. 880 881 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 882 883 History: 884 Added May 10, 2021 885 +/ 886 inout(ClassList) styleClassList() inout { 887 return cast(inout(ClassList)) ClassList(cast() this); 888 } 889 890 /++ 891 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. 892 893 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. 894 895 The upper 32 bits are available for your own extensions. 896 897 History: 898 Added May 10, 2021 899 +/ 900 enum DynamicState : ulong { 901 focus = (1 << 0), /// the widget currently has the keyboard focus 902 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 903 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 904 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 905 checked = (1 << 4), /// the widget is toggleable and currently toggled on 906 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 907 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 908 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 909 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. 910 911 USER_BEGIN = (1UL << 32), 912 } 913 914 // I want to add the primary and cancel styles to buttons at least at some point somehow. 915 916 /// ditto 917 @property ulong dynamicState() { return dynamicState_; } 918 /// ditto 919 @property ulong dynamicState(ulong newValue) { 920 if(dynamicState != newValue) { 921 auto old = dynamicState_; 922 dynamicState_ = newValue; 923 924 useStyleProperties((scope Widget.Style s) { 925 if(s.variesWithState(old ^ newValue)) 926 redraw(); 927 }); 928 } 929 return dynamicState_; 930 } 931 932 /// ditto 933 void setDynamicState(ulong flags, bool state) { 934 auto ds = dynamicState_; 935 if(state) 936 ds |= flags; 937 else 938 ds &= ~flags; 939 940 dynamicState = ds; 941 } 942 943 private ulong dynamicState_; 944 945 deprecated("Use dynamic styles instead now") { 946 Color backgroundColor() { return backgroundColor_; } 947 void backgroundColor(Color c){ this.backgroundColor_ = c; } 948 949 MouseCursor cursor() { return GenericCursor.Default; } 950 } private Color backgroundColor_ = Color.transparent; 951 952 953 /++ 954 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). 955 956 It is here so there can be a specificity switch. 957 958 See [OverrideStyle] for a helper function to use your own. 959 960 History: 961 Added May 11, 2021 962 +/ 963 static class Style/* : StyleProperties*/ { 964 public Widget widget; // public because the mixin template needs access to it 965 966 /++ 967 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 968 969 History: 970 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. 971 +/ 972 bool variesWithState(ulong dynamicStateFlags) { 973 version(win32_widgets) { 974 if(widget.hwnd) 975 return false; 976 } 977 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 978 } 979 980 /// 981 Color foregroundColor() { 982 return WidgetPainter.visualTheme.foregroundColor; 983 } 984 985 /// 986 WidgetBackground background() { 987 // the default is a "transparent" background, which means 988 // it goes as far up as it can to get the color 989 if (widget.backgroundColor_ != Color.transparent) 990 return WidgetBackground(widget.backgroundColor_); 991 if (widget.parent) 992 return widget.parent.getComputedStyle.background; 993 return WidgetBackground(widget.backgroundColor_); 994 } 995 996 private static OperatingSystemFont fontCached_; 997 private OperatingSystemFont fontCached() { 998 if(fontCached_ is null) 999 fontCached_ = font(); 1000 return fontCached_; 1001 } 1002 1003 /++ 1004 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. 1005 +/ 1006 OperatingSystemFont font() { 1007 return null; 1008 } 1009 1010 /++ 1011 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. 1012 1013 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1014 1015 History: 1016 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1017 +/ 1018 MouseCursor cursor() { 1019 return GenericCursor.Default; 1020 } 1021 1022 FrameStyle borderStyle() { 1023 return FrameStyle.none; 1024 } 1025 1026 /++ 1027 +/ 1028 Color borderColor() { 1029 return Color.transparent; 1030 } 1031 1032 FrameStyle outlineStyle() { 1033 if(widget.dynamicState & DynamicState.focus) 1034 return FrameStyle.dotted; 1035 else 1036 return FrameStyle.none; 1037 } 1038 1039 Color outlineColor() { 1040 return foregroundColor; 1041 } 1042 } 1043 1044 /++ 1045 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1046 The basic usage is simple: 1047 1048 --- 1049 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1050 // override style hints as-needed here 1051 } 1052 OverrideStyle!Style; // add the method 1053 --- 1054 1055 $(TIP 1056 While the class is not forced to be `static`, for best results, it should be. A non-static class 1057 can not be inherited by other objects whereas the static one can. A property on the base class, 1058 called [Widget.Style.widget|widget], is available for you to access its properties. 1059 ) 1060 1061 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1062 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1063 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1064 1065 1066 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1067 You may also just override `variesWithState` when you use this flag. 1068 1069 --- 1070 mixin OverrideStyle!( 1071 DynamicState.focus, YourFocusedStyle, 1072 DynamicState.hover, YourHoverStyle, 1073 YourDefaultStyle 1074 ) 1075 --- 1076 1077 It checks if `dynamicState` matches the state and if so, returns the object given. 1078 1079 If there is no state mask given, the next one matches everything. The first match given is used. 1080 1081 However, since in most cases you'll want check state inside your individual methods, you probably won't 1082 find much use for this whole-class swap out. 1083 1084 History: 1085 Added May 16, 2021 1086 +/ 1087 static protected mixin template OverrideStyle(S...) { 1088 static import amg = arsd.minigui; 1089 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1090 ulong mask = 0; 1091 foreach(idx, thing; S) { 1092 static if(is(typeof(thing) : ulong)) { 1093 mask = thing; 1094 } else { 1095 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1096 //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."); 1097 scope amg.Widget.Style s = new thing(); 1098 s.widget = this; 1099 dg(s); 1100 return; 1101 } 1102 } 1103 } 1104 } 1105 } 1106 /++ 1107 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1108 +/ 1109 void useStyleProperties(scope void delegate(scope Style props) dg) { 1110 scope Style s = new Style(); 1111 s.widget = this; 1112 dg(s); 1113 } 1114 1115 1116 protected void sendResizeEvent() { 1117 this.emit!ResizeEvent(); 1118 } 1119 1120 Menu contextMenu(int x, int y) { return null; } 1121 1122 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1123 if(parentWindow is null || parentWindow.win is null) return false; 1124 1125 auto menu = this.contextMenu(x, y); 1126 if(menu is null) 1127 return false; 1128 1129 version(win32_widgets) { 1130 // FIXME: if it is -1, -1, do it at the current selection location instead 1131 // tho the corner of the window, whcih it does now, isn't the literal worst. 1132 1133 if(screenX < 0 && screenY < 0) { 1134 auto p = this.globalCoordinates(); 1135 if(screenX == -2) 1136 p.x += x; 1137 if(screenY == -2) 1138 p.y += y; 1139 1140 screenX = p.x; 1141 screenY = p.y; 1142 } 1143 1144 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1145 throw new Exception("TrackContextMenuEx"); 1146 } else version(custom_widgets) { 1147 menu.popup(this, x, y); 1148 } 1149 1150 return true; 1151 } 1152 1153 /++ 1154 Removes this widget from its parent. 1155 1156 History: 1157 `removeWidget` was made `final` on May 11, 2021. 1158 +/ 1159 @scriptable 1160 final void removeWidget() { 1161 auto p = this.parent; 1162 if(p) { 1163 int item; 1164 for(item = 0; item < p._children.length; item++) 1165 if(p._children[item] is this) 1166 break; 1167 auto idx = item; 1168 for(; item < p._children.length - 1; item++) 1169 p._children[item] = p._children[item + 1]; 1170 p._children = p._children[0 .. $-1]; 1171 1172 this.parent.widgetRemoved(idx, this); 1173 //this.parent = null; 1174 1175 p.queueRecomputeChildLayout(); 1176 } 1177 version(win32_widgets) { 1178 removeAllChildren(); 1179 if(hwnd) { 1180 DestroyWindow(hwnd); 1181 hwnd = null; 1182 } 1183 } 1184 } 1185 1186 /++ 1187 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. 1188 1189 History: 1190 Added September 19, 2021 1191 +/ 1192 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1193 1194 /++ 1195 Removes all child widgets from `this`. You should not use the removed widgets again. 1196 1197 Note that on Windows, it also destroys the native handles for the removed children recursively. 1198 1199 History: 1200 Added July 1, 2021 (dub v10.2) 1201 +/ 1202 void removeAllChildren() { 1203 version(win32_widgets) 1204 foreach(child; _children) { 1205 child.removeAllChildren(); 1206 if(child.hwnd) { 1207 DestroyWindow(child.hwnd); 1208 child.hwnd = null; 1209 } 1210 } 1211 auto orig = this._children; 1212 this._children = null; 1213 foreach(idx, w; orig) 1214 this.widgetRemoved(idx, w); 1215 1216 queueRecomputeChildLayout(); 1217 } 1218 1219 /++ 1220 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1221 +/ 1222 @scriptable 1223 Widget getChildByName(string name) { 1224 return getByName(name); 1225 } 1226 /++ 1227 Finds the nearest descendant with the requested type and [name]. May return `this`. 1228 +/ 1229 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1230 if(this.name == name) 1231 if(auto c = cast(WidgetClass) this) 1232 return c; 1233 foreach(child; children) { 1234 auto w = child.getByName(name); 1235 if(auto c = cast(WidgetClass) w) 1236 return c; 1237 } 1238 return null; 1239 } 1240 1241 /++ 1242 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. 1243 Names should be unique in a window. 1244 1245 See_Also: [getByName], [getChildByName] 1246 +/ 1247 @scriptable string name; 1248 1249 private EventHandler[][string] bubblingEventHandlers; 1250 private EventHandler[][string] capturingEventHandlers; 1251 1252 /++ 1253 Default event handlers. These are called on the appropriate 1254 event unless [Event.preventDefault] is called on the event at 1255 some point through the bubbling process. 1256 1257 1258 If you are implementing your own widget and want to add custom 1259 events, you should follow the same pattern here: create a virtual 1260 function named `defaultEventHandler_eventname` with the implementation, 1261 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1262 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1263 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1264 This ensures virtual dispatch based on the correct subclass. 1265 1266 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1267 overridden version. 1268 1269 You only need to do that on parent classes adding NEW event types. If you 1270 just want to change the default behavior of an existing event type in a subclass, 1271 you override the function (and optionally call `super.method_name`) like normal. 1272 1273 +/ 1274 protected EventHandler[string] defaultEventHandlers; 1275 1276 /// ditto 1277 void setupDefaultEventHandlers() { 1278 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1279 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1280 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1281 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1282 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1283 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1284 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1285 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1286 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1287 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1288 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1289 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1290 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1291 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1292 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1293 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1294 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1295 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1296 } 1297 1298 /// ditto 1299 void defaultEventHandler_click(ClickEvent event) {} 1300 /// ditto 1301 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1302 /// ditto 1303 void defaultEventHandler_keydown(KeyDownEvent event) {} 1304 /// ditto 1305 void defaultEventHandler_keyup(KeyUpEvent event) {} 1306 /// ditto 1307 void defaultEventHandler_mousedown(MouseDownEvent event) { 1308 if(event.button == MouseButton.left) { 1309 if(this.tabStop) 1310 this.focus(); 1311 } 1312 } 1313 /// ditto 1314 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1315 /// ditto 1316 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1317 /// ditto 1318 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1319 /// ditto 1320 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1321 /// ditto 1322 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1323 /// ditto 1324 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1325 /// ditto 1326 void defaultEventHandler_char(CharEvent event) {} 1327 /// ditto 1328 void defaultEventHandler_triggered(Event event) {} 1329 /// ditto 1330 void defaultEventHandler_change(Event event) {} 1331 /// ditto 1332 void defaultEventHandler_focus(Event event) {} 1333 /// ditto 1334 void defaultEventHandler_blur(Event event) {} 1335 /// ditto 1336 void defaultEventHandler_focusin(Event event) {} 1337 /// ditto 1338 void defaultEventHandler_focusout(Event event) {} 1339 1340 /++ 1341 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1342 1343 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1344 1345 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1346 of participating in handler delegation. 1347 1348 $(TIP 1349 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. 1350 ) 1351 +/ 1352 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1353 return addEventListener(event, (Widget, scope Event e) { 1354 if(e.srcElement is this) 1355 handler(); 1356 }, useCapture); 1357 } 1358 1359 /// ditto 1360 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1361 return addEventListener(event, (Widget, Event e) { 1362 if(e.srcElement is this) 1363 handler(e); 1364 }, useCapture); 1365 } 1366 1367 /// ditto 1368 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1369 static if(is(Handler Fn == delegate)) { 1370 static if(is(Fn Params == __parameters)) { 1371 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1372 if(e.srcElement !is this) 1373 return; 1374 auto ty = cast(Params[0]) e; 1375 if(ty !is null) 1376 handler(ty); 1377 }, useCapture); 1378 } else static assert(0); 1379 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1380 } 1381 1382 /// ditto 1383 @scriptable 1384 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1385 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1386 } 1387 1388 /// ditto 1389 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1390 static if(is(Handler Fn == delegate)) { 1391 static if(is(Fn Params == __parameters)) { 1392 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1393 auto ty = cast(Params[0]) e; 1394 if(ty !is null) 1395 handler(ty); 1396 }, useCapture); 1397 } else static assert(0); 1398 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1399 } 1400 1401 /// ditto 1402 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1403 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1404 } 1405 1406 /// ditto 1407 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1408 if(event.length > 2 && event[0..2] == "on") 1409 event = event[2 .. $]; 1410 1411 if(useCapture) 1412 capturingEventHandlers[event] ~= handler; 1413 else 1414 bubblingEventHandlers[event] ~= handler; 1415 1416 return EventListener(this, event, handler, useCapture); 1417 } 1418 1419 /// ditto 1420 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1421 if(event.length > 2 && event[0..2] == "on") 1422 event = event[2 .. $]; 1423 1424 if(useCapture) { 1425 if(event in capturingEventHandlers) 1426 foreach(ref evt; capturingEventHandlers[event]) 1427 if(evt is handler) evt = null; 1428 } else { 1429 if(event in bubblingEventHandlers) 1430 foreach(ref evt; bubblingEventHandlers[event]) 1431 if(evt is handler) evt = null; 1432 } 1433 } 1434 1435 /// ditto 1436 void removeEventListener(EventListener listener) { 1437 removeEventListener(listener.event, listener.handler, listener.useCapture); 1438 } 1439 1440 static if(UsingSimpledisplayX11) { 1441 void discardXConnectionState() { 1442 foreach(child; children) 1443 child.discardXConnectionState(); 1444 } 1445 1446 void recreateXConnectionState() { 1447 foreach(child; children) 1448 child.recreateXConnectionState(); 1449 redraw(); 1450 } 1451 } 1452 1453 /++ 1454 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1455 1456 History: 1457 `globalCoordinates` was made `final` on May 11, 2021. 1458 +/ 1459 Point globalCoordinates() { 1460 int x = this.x; 1461 int y = this.y; 1462 auto p = this.parent; 1463 while(p) { 1464 x += p.x; 1465 y += p.y; 1466 p = p.parent; 1467 } 1468 1469 static if(UsingSimpledisplayX11) { 1470 auto dpy = XDisplayConnection.get; 1471 arsd.simpledisplay.Window dummyw; 1472 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1473 } else version(Windows) { 1474 POINT pt; 1475 pt.x = x; 1476 pt.y = y; 1477 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1478 x = pt.x; 1479 y = pt.y; 1480 } else { 1481 featureNotImplemented(); 1482 } 1483 1484 return Point(x, y); 1485 } 1486 1487 version(win32_widgets) 1488 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1489 1490 version(win32_widgets) 1491 /// Called when a WM_COMMAND is sent to the associated hwnd. 1492 void handleWmCommand(ushort cmd, ushort id) {} 1493 1494 version(win32_widgets) 1495 /++ 1496 Called when a WM_NOTIFY is sent to the associated hwnd. 1497 1498 History: 1499 +/ 1500 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1501 1502 version(win32_widgets) 1503 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); } 1504 1505 /++ 1506 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1507 1508 Updates to this variable will only be made visible on the next mouse enter event. 1509 +/ 1510 @scriptable string statusTip; 1511 // string toolTip; 1512 // string helpText; 1513 1514 /++ 1515 If true, this widget can be focused via keyboard control with the tab key. 1516 1517 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1518 +/ 1519 bool tabStop = true; 1520 /++ 1521 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.) 1522 +/ 1523 int tabOrder; 1524 1525 version(win32_widgets) { 1526 static Widget[HWND] nativeMapping; 1527 /// The native handle, if there is one. 1528 HWND hwnd; 1529 WNDPROC originalWindowProcedure; 1530 1531 SimpleWindow simpleWindowWrappingHwnd; 1532 1533 // please note it IGNORES your return value and does NOT forward it to Windows! 1534 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1535 return 0; 1536 } 1537 } 1538 private bool implicitlyCreated; 1539 1540 /// 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. 1541 int x; 1542 /// ditto 1543 int y; 1544 private int _width; 1545 private int _height; 1546 private Widget[] _children; 1547 private Widget _parent; 1548 private Window _parentWindow; 1549 1550 /++ 1551 Returns the window to which this widget is attached. 1552 1553 History: 1554 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. 1555 +/ 1556 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1557 private @property void parentWindow(Window parent) { 1558 _parentWindow = parent; 1559 foreach(child; children) 1560 child.parentWindow = parent; // please note that this is recursive 1561 } 1562 1563 /++ 1564 Returns the list of the widget's children. 1565 1566 History: 1567 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1568 1569 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1570 +/ 1571 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1572 1573 /++ 1574 Returns the widget's parent. 1575 1576 History: 1577 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1578 1579 The parent should only be managed by the [addChild] and [removeWidget] method. 1580 +/ 1581 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1582 1583 /// The widget's current size. 1584 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1585 /// ditto 1586 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1587 1588 /// Only the layout manager should be calling these. 1589 final protected @property int width(int a) @safe { return _width = a; } 1590 /// ditto 1591 final protected @property int height(int a) @safe { return _height = a; } 1592 1593 /++ 1594 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. 1595 1596 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1597 +/ 1598 protected void registerMovement() { 1599 version(win32_widgets) { 1600 if(hwnd) { 1601 auto pos = getChildPositionRelativeToParentHwnd(this); 1602 MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 1603 this.redraw(); 1604 } 1605 } 1606 sendResizeEvent(); 1607 } 1608 1609 /// Creates the widget and adds it to the parent. 1610 this(Widget parent) { 1611 if(parent !is null) 1612 parent.addChild(this); 1613 setupDefaultEventHandlers(); 1614 } 1615 1616 /// 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. 1617 @scriptable 1618 bool isFocused() { 1619 return parentWindow && parentWindow.focusedWidget is this; 1620 } 1621 1622 private bool showing_ = true; 1623 /// 1624 bool showing() { return showing_; } 1625 /// 1626 bool hidden() { return !showing_; } 1627 /++ 1628 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. 1629 +/ 1630 void showing(bool s, bool recalculate = true) { 1631 auto so = showing_; 1632 showing_ = s; 1633 if(s != so) { 1634 version(win32_widgets) 1635 if(hwnd) 1636 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1637 1638 if(parent && recalculate) { 1639 parent.queueRecomputeChildLayout(); 1640 parent.redraw(); 1641 } 1642 1643 foreach(child; children) 1644 child.showing(s, false); 1645 1646 } 1647 queueRecomputeChildLayout(); 1648 redraw(); 1649 } 1650 /// Convenience method for `showing = true` 1651 @scriptable 1652 void show() { 1653 showing = true; 1654 } 1655 /// Convenience method for `showing = false` 1656 @scriptable 1657 void hide() { 1658 showing = false; 1659 } 1660 1661 /// 1662 @scriptable 1663 void focus() { 1664 assert(parentWindow !is null); 1665 if(isFocused()) 1666 return; 1667 1668 if(parentWindow.focusedWidget) { 1669 // FIXME: more details here? like from and to 1670 auto from = parentWindow.focusedWidget; 1671 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1672 parentWindow.focusedWidget = null; 1673 from.emit!BlurEvent(); 1674 this.emit!FocusOutEvent(); 1675 } 1676 1677 1678 version(win32_widgets) { 1679 if(this.hwnd !is null) 1680 SetFocus(this.hwnd); 1681 } 1682 //else static if(UsingSimpledisplayX11) 1683 //this.parentWindow.win.focus(); 1684 1685 parentWindow.focusedWidget = this; 1686 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1687 this.emit!FocusEvent(); 1688 this.emit!FocusInEvent(); 1689 } 1690 1691 /+ 1692 /++ 1693 Unfocuses the widget. This may reset 1694 +/ 1695 @scriptable 1696 void blur() { 1697 1698 } 1699 +/ 1700 1701 1702 /++ 1703 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1704 1705 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1706 +/ 1707 void attachedToWindow(Window w) {} 1708 /++ 1709 Callback when the widget is added to another widget. 1710 1711 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1712 +/ 1713 void addedTo(Widget w) {} 1714 1715 /++ 1716 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. 1717 1718 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1719 +/ 1720 protected void addChild(Widget w, int position = int.max) { 1721 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1722 assert(w !is this, "Child cannot be its own parent!"); 1723 w._parent = this; 1724 if(position == int.max || position == children.length) { 1725 _children ~= w; 1726 } else { 1727 assert(position < _children.length); 1728 _children.length = _children.length + 1; 1729 for(int i = cast(int) _children.length - 1; i > position; i--) 1730 _children[i] = _children[i - 1]; 1731 _children[position] = w; 1732 } 1733 1734 this.parentWindow = this._parentWindow; 1735 1736 w.addedTo(this); 1737 1738 if(this.hidden) 1739 w.showing = false; 1740 1741 if(parentWindow !is null) { 1742 w.attachedToWindow(parentWindow); 1743 parentWindow.queueRecomputeChildLayout(); 1744 parentWindow.redraw(); 1745 } 1746 } 1747 1748 /++ 1749 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. 1750 +/ 1751 Widget getChildAtPosition(int x, int y) { 1752 // it goes backward so the last one to show gets picked first 1753 // might use z-index later 1754 foreach_reverse(child; children) { 1755 if(child.hidden) 1756 continue; 1757 if(child.x <= x && child.y <= y 1758 && ((x - child.x) < child.width) 1759 && ((y - child.y) < child.height)) 1760 { 1761 return child; 1762 } 1763 } 1764 1765 return null; 1766 } 1767 1768 /++ 1769 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. 1770 1771 History: 1772 Added July 2, 2021 (v10.2) 1773 +/ 1774 protected void addScrollPosition(ref int x, ref int y) {}; 1775 1776 /++ 1777 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. 1778 1779 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. 1780 1781 [paint] is not called for system widgets as the OS library draws them instead. 1782 1783 1784 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. 1785 1786 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1787 1788 History: 1789 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. 1790 +/ 1791 void paint(WidgetPainter painter) { 1792 version(win32_widgets) 1793 if(hwnd) { 1794 return; 1795 } 1796 painter.drawThemed(&paintContent); // note this refers to the following overload 1797 } 1798 1799 /++ 1800 Responsible for drawing the content as the theme engine is responsible for other elements. 1801 1802 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1803 1804 Params: 1805 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. 1806 1807 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. 1808 1809 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. 1810 1811 Returns: 1812 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. 1813 1814 History: 1815 Added May 15, 2021 1816 +/ 1817 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1818 return bounds; 1819 } 1820 1821 deprecated("Change ScreenPainter to WidgetPainter") 1822 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1823 1824 /// I don't actually like the name of this 1825 /// this draws a background on it 1826 void erase(WidgetPainter painter) { 1827 version(win32_widgets) 1828 if(hwnd) return; // Windows will do it. I think. 1829 1830 auto c = getComputedStyle().background.color; 1831 painter.fillColor = c; 1832 painter.outlineColor = c; 1833 1834 version(win32_widgets) { 1835 HANDLE b, p; 1836 if(c.a == 0 && parent is parentWindow) { 1837 // I don't remember why I had this really... 1838 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1839 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1840 } 1841 } 1842 painter.drawRectangle(Point(0, 0), width, height); 1843 version(win32_widgets) { 1844 if(c.a == 0 && parent is parentWindow) { 1845 SelectObject(painter.impl.hdc, p); 1846 SelectObject(painter.impl.hdc, b); 1847 } 1848 } 1849 } 1850 1851 /// 1852 WidgetPainter draw() { 1853 int x = this.x, y = this.y; 1854 auto parent = this.parent; 1855 while(parent) { 1856 x += parent.x; 1857 y += parent.y; 1858 parent = parent.parent; 1859 } 1860 1861 auto painter = parentWindow.win.draw(true); 1862 painter.originX = x; 1863 painter.originY = y; 1864 painter.setClipRectangle(Point(0, 0), width, height); 1865 return WidgetPainter(painter, this); 1866 } 1867 1868 /// 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. 1869 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1870 if(hidden) 1871 return; 1872 1873 int paintX = x; 1874 int paintY = y; 1875 if(this.useNativeDrawing()) { 1876 paintX = 0; 1877 paintY = 0; 1878 lox = 0; 1879 loy = 0; 1880 containment = Rectangle(0, 0, int.max, int.max); 1881 } 1882 1883 painter.originX = lox + paintX; 1884 painter.originY = loy + paintY; 1885 1886 bool actuallyPainted = false; 1887 1888 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1889 if(clip == Rectangle.init) { 1890 // writeln(this, " clipped out"); 1891 return; 1892 } 1893 1894 bool invalidateChildren = invalidate; 1895 1896 if(redrawRequested || force) { 1897 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1898 1899 painter.drawingUpon = this; 1900 1901 erase(painter); 1902 if(painter.visualTheme) 1903 painter.visualTheme.doPaint(this, painter); 1904 else 1905 paint(painter); 1906 1907 if(invalidate) { 1908 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1909 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1910 painter.invalidateRect(region); 1911 // children are contained inside this, so no need to do extra work 1912 invalidateChildren = false; 1913 } 1914 1915 redrawRequested = false; 1916 actuallyPainted = true; 1917 } 1918 1919 foreach(child; children) { 1920 version(win32_widgets) 1921 if(child.useNativeDrawing()) continue; 1922 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1923 } 1924 1925 version(win32_widgets) 1926 foreach(child; children) { 1927 if(child.useNativeDrawing) { 1928 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1929 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 1930 } 1931 } 1932 } 1933 1934 protected bool useNativeDrawing() nothrow { 1935 version(win32_widgets) 1936 return hwnd !is null; 1937 else 1938 return false; 1939 } 1940 1941 private static class RedrawEvent {} 1942 private __gshared re = new RedrawEvent(); 1943 1944 private bool redrawRequested; 1945 /// 1946 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1947 redrawRequested = true; 1948 1949 if(this.parentWindow) { 1950 auto sw = this.parentWindow.win; 1951 assert(sw !is null); 1952 if(!sw.eventQueued!RedrawEvent) { 1953 sw.postEvent(re); 1954 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1955 } 1956 } 1957 } 1958 1959 private SimpleWindow drawableWindow; 1960 1961 /++ 1962 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. 1963 1964 Returns: 1965 `true` if you should do your default behavior. 1966 1967 History: 1968 Added May 5, 2021 1969 1970 Bugs: 1971 It does not do the static checks on gdc right now. 1972 +/ 1973 final protected bool emit(EventType, this This, Args...)(Args args) { 1974 version(GNU) {} else 1975 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1976 auto e = new EventType(this, args); 1977 e.dispatch(); 1978 return !e.defaultPrevented; 1979 } 1980 /// ditto 1981 final protected bool emit(string eventString, this This)() { 1982 auto e = new Event(eventString, this); 1983 e.dispatch(); 1984 return !e.defaultPrevented; 1985 } 1986 1987 /++ 1988 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. 1989 1990 History: 1991 Added May 5, 2021 1992 +/ 1993 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1994 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1995 return addEventListener(handler); 1996 } 1997 1998 /++ 1999 Gets the computed style properties from the visual theme. 2000 2001 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].) 2002 2003 History: 2004 Added May 8, 2021 2005 +/ 2006 final StyleInformation getComputedStyle() { 2007 return StyleInformation(this); 2008 } 2009 2010 int focusableWidgets(scope int delegate(Widget) dg) { 2011 foreach(widget; WidgetStream(this)) { 2012 if(widget.tabStop && !widget.hidden) { 2013 int result = dg(widget); 2014 if (result) 2015 return result; 2016 } 2017 } 2018 return 0; 2019 } 2020 2021 /++ 2022 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2023 for the given content box (the area between the padding) 2024 2025 History: 2026 Added January 4, 2023 (dub v11.0) 2027 +/ 2028 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2029 auto cs = getComputedStyle(); 2030 2031 auto borderWidth = getBorderWidth(cs.borderStyle); 2032 2033 auto rect = contentBox; 2034 2035 rect.left -= borderWidth; 2036 rect.right += borderWidth; 2037 rect.top -= borderWidth; 2038 rect.bottom += borderWidth; 2039 2040 auto insideBorderRect = rect; 2041 2042 rect.left -= cs.paddingLeft; 2043 rect.right += cs.paddingRight; 2044 rect.top -= cs.paddingTop; 2045 rect.bottom += cs.paddingBottom; 2046 2047 return rect; 2048 } 2049 2050 2051 // FIXME: I kinda want to hide events from implementation widgets 2052 // so it just catches them all and stops propagation... 2053 // i guess i can do it with a event listener on star. 2054 2055 mixin Emits!KeyDownEvent; /// 2056 mixin Emits!KeyUpEvent; /// 2057 mixin Emits!CharEvent; /// 2058 2059 mixin Emits!MouseDownEvent; /// 2060 mixin Emits!MouseUpEvent; /// 2061 mixin Emits!ClickEvent; /// 2062 mixin Emits!DoubleClickEvent; /// 2063 mixin Emits!MouseMoveEvent; /// 2064 mixin Emits!MouseOverEvent; /// 2065 mixin Emits!MouseOutEvent; /// 2066 mixin Emits!MouseEnterEvent; /// 2067 mixin Emits!MouseLeaveEvent; /// 2068 2069 mixin Emits!ResizeEvent; /// 2070 2071 mixin Emits!BlurEvent; /// 2072 mixin Emits!FocusEvent; /// 2073 2074 mixin Emits!FocusInEvent; /// 2075 mixin Emits!FocusOutEvent; /// 2076 } 2077 2078 /+ 2079 /++ 2080 Interface to indicate that the widget has a simple value property. 2081 2082 History: 2083 Added August 26, 2021 2084 +/ 2085 interface HasValue!T { 2086 /// Getter 2087 @property T value(); 2088 /// Setter 2089 @property void value(T); 2090 } 2091 2092 /++ 2093 Interface to indicate that the widget has a range of possible values for its simple value property. 2094 This would be present on something like a slider or possibly a number picker. 2095 2096 History: 2097 Added September 11, 2021 2098 +/ 2099 interface HasRangeOfValues!T : HasValue!T { 2100 /// The minimum and maximum values in the range, inclusive. 2101 @property T minValue(); 2102 @property void minValue(T); /// ditto 2103 @property T maxValue(); /// ditto 2104 @property void maxValue(T); /// ditto 2105 2106 /// The smallest step the user interface allows. User may still type in values without this limitation. 2107 @property void step(T); 2108 @property T step(); /// ditto 2109 } 2110 2111 /++ 2112 Interface to indicate that the widget has a list of possible values the user can choose from. 2113 This would be present on something like a drop-down selector. 2114 2115 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2116 combobox. 2117 2118 History: 2119 Added September 11, 2021 2120 +/ 2121 interface HasListOfValues!T : HasValue!T { 2122 @property T[] values; 2123 @property void values(T[]); 2124 2125 @property int selectedIndex(); // note it may return -1! 2126 @property void selectedIndex(int); 2127 } 2128 +/ 2129 2130 /++ 2131 History: 2132 Added September 2021 (dub v10.4) 2133 +/ 2134 class GridLayout : Layout { 2135 2136 // 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. 2137 2138 /++ 2139 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2140 +/ 2141 enum Gravity { 2142 Center = 0, 2143 NorthWest = North | West, 2144 North = 0b10_00, 2145 NorthEast = North | East, 2146 West = 0b00_10, 2147 East = 0b00_01, 2148 SouthWest = South | West, 2149 South = 0b01_00, 2150 SouthEast = South | East, 2151 } 2152 2153 /++ 2154 The width and height are in some proportional units and can often just be 12. 2155 +/ 2156 this(int width, int height, Widget parent) { 2157 this.gridWidth = width; 2158 this.gridHeight = height; 2159 super(parent); 2160 } 2161 2162 /++ 2163 Sets the position of the given child. 2164 2165 The units of these arguments are in the proportional grid units you set in the constructor. 2166 +/ 2167 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2168 // ensure it is in bounds 2169 // then ensure no overlaps 2170 2171 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2172 2173 foreach(ref position; positions) { 2174 if(position.widget is child) { 2175 position = p; 2176 goto set; 2177 } 2178 } 2179 2180 positions ~= p; 2181 2182 set: 2183 2184 // FIXME: should this batch? 2185 queueRecomputeChildLayout(); 2186 2187 return child; 2188 } 2189 2190 override void addChild(Widget w, int position = int.max) { 2191 super.addChild(w, position); 2192 //positions ~= ChildPosition(w); 2193 if(position != int.max) { 2194 // FIXME: align it so they actually match. 2195 } 2196 } 2197 2198 override void widgetRemoved(size_t idx, Widget w) { 2199 // FIXME: keep the positions array aligned 2200 // positions[idx].widget = null; 2201 } 2202 2203 override void recomputeChildLayout() { 2204 registerMovement(); 2205 int onGrid = cast(int) positions.length; 2206 c: foreach(child; children) { 2207 // just snap it to the grid 2208 if(onGrid) 2209 foreach(position; positions) 2210 if(position.widget is child) { 2211 child.x = this.width * position.x / this.gridWidth; 2212 child.y = this.height * position.y / this.gridHeight; 2213 child.width = this.width * position.width / this.gridWidth; 2214 child.height = this.height * position.height / this.gridHeight; 2215 2216 auto diff = child.width - child.maxWidth(); 2217 // FIXME: gravity? 2218 if(diff > 0) { 2219 child.width = child.width - diff; 2220 2221 if(position.gravity & Gravity.West) { 2222 // nothing needed, already aligned 2223 } else if(position.gravity & Gravity.East) { 2224 child.x += diff; 2225 } else { 2226 child.x += diff / 2; 2227 } 2228 } 2229 2230 diff = child.height - child.maxHeight(); 2231 // FIXME: gravity? 2232 if(diff > 0) { 2233 child.height = child.height - diff; 2234 2235 if(position.gravity & Gravity.North) { 2236 // nothing needed, already aligned 2237 } else if(position.gravity & Gravity.South) { 2238 child.y += diff; 2239 } else { 2240 child.y += diff / 2; 2241 } 2242 } 2243 2244 2245 child.recomputeChildLayout(); 2246 onGrid--; 2247 continue c; 2248 } 2249 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2250 } 2251 } 2252 2253 private struct ChildPosition { 2254 Widget widget; 2255 int x; 2256 int y; 2257 int width; 2258 int height; 2259 Gravity gravity; 2260 } 2261 private ChildPosition[] positions; 2262 2263 int gridWidth = 12; 2264 int gridHeight = 12; 2265 } 2266 2267 /// 2268 abstract class ComboboxBase : Widget { 2269 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2270 // or to always show the list, we want CBS_SIMPLE == 1 2271 version(win32_widgets) 2272 this(uint style, Widget parent) { 2273 super(parent); 2274 createWin32Window(this, "ComboBox"w, null, style); 2275 } 2276 else version(custom_widgets) 2277 this(Widget parent) { 2278 super(parent); 2279 2280 addEventListener((KeyDownEvent event) { 2281 if(event.key == Key.Up) { 2282 if(selection_ > -1) { // -1 means select blank 2283 selection_--; 2284 fireChangeEvent(); 2285 } 2286 event.preventDefault(); 2287 } 2288 if(event.key == Key.Down) { 2289 if(selection_ + 1 < options.length) { 2290 selection_++; 2291 fireChangeEvent(); 2292 } 2293 event.preventDefault(); 2294 } 2295 2296 }); 2297 2298 } 2299 else static assert(false); 2300 2301 /++ 2302 Returns the current list of options in the selection. 2303 2304 History: 2305 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2306 +/ 2307 final @property string[] options() const { 2308 return cast(string[]) options_; 2309 } 2310 2311 private string[] options_; 2312 private int selection_ = -1; 2313 2314 /++ 2315 Adds an option to the end of options array. 2316 +/ 2317 void addOption(string s) { 2318 options_ ~= s; 2319 version(win32_widgets) 2320 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2321 } 2322 2323 /++ 2324 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2325 +/ 2326 int getSelection() { 2327 return selection_; 2328 } 2329 2330 /++ 2331 Returns the current selection as a string. 2332 2333 History: 2334 Added November 17, 2021 2335 +/ 2336 string getSelectionString() { 2337 return selection_ == -1 ? null : options[selection_]; 2338 } 2339 2340 /++ 2341 Sets the current selection to an index in the options array, or to the given option if present. 2342 Please note that the string version may do a linear lookup. 2343 2344 Returns: 2345 the index you passed in 2346 2347 History: 2348 The `string` based overload was added on March 1, 2022 (dub v10.7). 2349 2350 The return value was `void` prior to March 1, 2022. 2351 +/ 2352 int setSelection(int idx) { 2353 selection_ = idx; 2354 version(win32_widgets) 2355 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2356 2357 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2358 t.dispatch(); 2359 2360 return idx; 2361 } 2362 2363 /// ditto 2364 int setSelection(string s) { 2365 if(s !is null) 2366 foreach(idx, item; options) 2367 if(item == s) { 2368 return setSelection(cast(int) idx); 2369 } 2370 return setSelection(-1); 2371 } 2372 2373 /++ 2374 This event is fired when the selection changes. Note it inherits 2375 from ChangeEvent!string, meaning you can use that as well, and it also 2376 fills in [Event.intValue]. 2377 +/ 2378 static class SelectionChangedEvent : ChangeEvent!string { 2379 this(Widget target, int iv, string sv) { 2380 super(target, &stringValue); 2381 this.iv = iv; 2382 this.sv = sv; 2383 } 2384 immutable int iv; 2385 immutable string sv; 2386 2387 override @property string stringValue() { return sv; } 2388 override @property int intValue() { return iv; } 2389 } 2390 2391 version(win32_widgets) 2392 override void handleWmCommand(ushort cmd, ushort id) { 2393 if(cmd == CBN_SELCHANGE) { 2394 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2395 fireChangeEvent(); 2396 } 2397 } 2398 2399 private void fireChangeEvent() { 2400 if(selection_ >= options.length) 2401 selection_ = -1; 2402 2403 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2404 t.dispatch(); 2405 } 2406 2407 version(win32_widgets) { 2408 override int minHeight() { return defaultLineHeight + 6; } 2409 override int maxHeight() { return defaultLineHeight + 6; } 2410 } else { 2411 override int minHeight() { return defaultLineHeight + 4; } 2412 override int maxHeight() { return defaultLineHeight + 4; } 2413 } 2414 2415 version(custom_widgets) { 2416 2417 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2418 2419 SimpleWindow dropDown; 2420 void popup() { 2421 auto w = width; 2422 // FIXME: suggestedDropdownHeight see below 2423 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2424 2425 auto coord = this.globalCoordinates(); 2426 auto dropDown = new SimpleWindow( 2427 w, h, 2428 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2429 2430 dropDown.move(coord.x, coord.y + this.height); 2431 2432 { 2433 auto cs = getComputedStyle(); 2434 auto painter = dropDown.draw(); 2435 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2436 auto p = Point(4, 4); 2437 painter.outlineColor = cs.foregroundColor; 2438 foreach(option; options) { 2439 painter.drawText(p, option); 2440 p.y += defaultLineHeight; 2441 } 2442 } 2443 2444 dropDown.setEventHandlers( 2445 (MouseEvent event) { 2446 if(event.type == MouseEventType.buttonReleased) { 2447 dropDown.close(); 2448 auto element = (event.y - 4) / defaultLineHeight; 2449 if(element >= 0 && element <= options.length) { 2450 selection_ = element; 2451 2452 fireChangeEvent(); 2453 } 2454 } 2455 } 2456 ); 2457 2458 dropDown.visibilityChanged = (bool visible) { 2459 if(visible) { 2460 this.redraw(); 2461 dropDown.grabInput(); 2462 } else { 2463 dropDown.releaseInputGrab(); 2464 } 2465 }; 2466 2467 dropDown.show(); 2468 } 2469 2470 } 2471 } 2472 2473 /++ 2474 A drop-down list where the user must select one of the 2475 given options. Like `<select>` in HTML. 2476 +/ 2477 class DropDownSelection : ComboboxBase { 2478 this(Widget parent) { 2479 version(win32_widgets) 2480 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2481 else version(custom_widgets) { 2482 super(parent); 2483 2484 addEventListener("focus", () { this.redraw; }); 2485 addEventListener("blur", () { this.redraw; }); 2486 addEventListener(EventType.change, () { this.redraw; }); 2487 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2488 addEventListener((KeyDownEvent event) { 2489 if(event.key == Key.Space) 2490 popup(); 2491 }); 2492 } else static assert(false); 2493 } 2494 2495 mixin Padding!q{2}; 2496 static class Style : Widget.Style { 2497 override FrameStyle borderStyle() { return FrameStyle.risen; } 2498 } 2499 mixin OverrideStyle!Style; 2500 2501 version(custom_widgets) 2502 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2503 auto cs = getComputedStyle(); 2504 2505 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2506 2507 painter.outlineColor = cs.foregroundColor; 2508 painter.fillColor = cs.foregroundColor; 2509 2510 /+ 2511 Point[4] triangle; 2512 enum padding = 6; 2513 enum paddingV = 7; 2514 enum triangleWidth = 10; 2515 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2516 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2517 triangle[2] = Point(width - padding - 0, paddingV); 2518 triangle[3] = triangle[0]; 2519 painter.drawPolygon(triangle[]); 2520 +/ 2521 2522 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2523 2524 painter.drawPolygon( 2525 scaleWithDpi(Point(2, 6) + offset), 2526 scaleWithDpi(Point(7, 11) + offset), 2527 scaleWithDpi(Point(12, 6) + offset), 2528 scaleWithDpi(Point(2, 6) + offset) 2529 ); 2530 2531 2532 return bounds; 2533 } 2534 2535 version(win32_widgets) 2536 override void registerMovement() { 2537 version(win32_widgets) { 2538 if(hwnd) { 2539 auto pos = getChildPositionRelativeToParentHwnd(this); 2540 // the height given to this from Windows' perspective is supposed 2541 // to include the drop down's height. so I add to it to give some 2542 // room for that. 2543 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2544 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2545 } 2546 } 2547 sendResizeEvent(); 2548 } 2549 } 2550 2551 /++ 2552 A text box with a drop down arrow listing selections. 2553 The user can choose from the list, or type their own. 2554 +/ 2555 class FreeEntrySelection : ComboboxBase { 2556 this(Widget parent) { 2557 version(win32_widgets) 2558 super(2 /* CBS_DROPDOWN */, parent); 2559 else version(custom_widgets) { 2560 super(parent); 2561 auto hl = new HorizontalLayout(this); 2562 lineEdit = new LineEdit(hl); 2563 2564 tabStop = false; 2565 2566 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2567 2568 auto btn = new class ArrowButton { 2569 this() { 2570 super(ArrowDirection.down, hl); 2571 } 2572 override int maxHeight() { 2573 return lineEdit.maxHeight; 2574 } 2575 }; 2576 //btn.addDirectEventListener("focus", &lineEdit.focus); 2577 btn.addEventListener("triggered", &this.popup); 2578 addEventListener(EventType.change, (Event event) { 2579 lineEdit.content = event.stringValue; 2580 lineEdit.focus(); 2581 redraw(); 2582 }); 2583 } 2584 else static assert(false); 2585 } 2586 2587 version(custom_widgets) { 2588 LineEdit lineEdit; 2589 } 2590 } 2591 2592 /++ 2593 A combination of free entry with a list below it. 2594 +/ 2595 class ComboBox : ComboboxBase { 2596 this(Widget parent) { 2597 version(win32_widgets) 2598 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2599 else version(custom_widgets) { 2600 super(parent); 2601 lineEdit = new LineEdit(this); 2602 listWidget = new ListWidget(this); 2603 listWidget.multiSelect = false; 2604 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2605 string c = null; 2606 foreach(option; listWidget.options) 2607 if(option.selected) { 2608 c = option.label; 2609 break; 2610 } 2611 lineEdit.content = c; 2612 }); 2613 2614 listWidget.tabStop = false; 2615 this.tabStop = false; 2616 listWidget.addEventListener("focus", &lineEdit.focus); 2617 this.addEventListener("focus", &lineEdit.focus); 2618 2619 addDirectEventListener(EventType.change, { 2620 listWidget.setSelection(selection_); 2621 if(selection_ != -1) 2622 lineEdit.content = options[selection_]; 2623 lineEdit.focus(); 2624 redraw(); 2625 }); 2626 2627 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2628 2629 listWidget.addDirectEventListener(EventType.change, { 2630 int set = -1; 2631 foreach(idx, opt; listWidget.options) 2632 if(opt.selected) { 2633 set = cast(int) idx; 2634 break; 2635 } 2636 if(set != selection_) 2637 this.setSelection(set); 2638 }); 2639 } else static assert(false); 2640 } 2641 2642 override int minHeight() { return defaultLineHeight * 3; } 2643 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2644 override int heightStretchiness() { return 5; } 2645 2646 version(custom_widgets) { 2647 LineEdit lineEdit; 2648 ListWidget listWidget; 2649 2650 override void addOption(string s) { 2651 listWidget.options ~= ListWidget.Option(s); 2652 ComboboxBase.addOption(s); 2653 } 2654 } 2655 } 2656 2657 /+ 2658 class Spinner : Widget { 2659 version(win32_widgets) 2660 this(Widget parent) { 2661 super(parent); 2662 parentWindow = parent.parentWindow; 2663 auto hlayout = new HorizontalLayout(this); 2664 lineEdit = new LineEdit(hlayout); 2665 upDownControl = new UpDownControl(hlayout); 2666 } 2667 2668 LineEdit lineEdit; 2669 UpDownControl upDownControl; 2670 } 2671 2672 class UpDownControl : Widget { 2673 version(win32_widgets) 2674 this(Widget parent) { 2675 super(parent); 2676 parentWindow = parent.parentWindow; 2677 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2678 } 2679 2680 override int minHeight() { return defaultLineHeight; } 2681 override int maxHeight() { return defaultLineHeight * 3/2; } 2682 2683 override int minWidth() { return defaultLineHeight * 3/2; } 2684 override int maxWidth() { return defaultLineHeight * 3/2; } 2685 } 2686 +/ 2687 2688 /+ 2689 class DataView : Widget { 2690 // this is the omnibus data viewer 2691 // the internal data layout is something like: 2692 // string[string][] but also each node can have parents 2693 } 2694 +/ 2695 2696 2697 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2698 2699 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2700 2701 // FIXME: menus should prolly capture the mouse. ugh i kno. 2702 /* 2703 TextEdit needs: 2704 2705 * caret manipulation 2706 * selection control 2707 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2708 2709 For example: 2710 2711 connect(paste, &textEdit.insertTextAtCaret); 2712 2713 would be nice. 2714 2715 2716 2717 I kinda want an omnibus dataview that combines list, tree, 2718 and table - it can be switched dynamically between them. 2719 2720 Flattening policy: only show top level, show recursive, show grouped 2721 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2722 2723 Single select, multi select, organization, drag+drop 2724 */ 2725 2726 //static if(UsingSimpledisplayX11) 2727 version(win32_widgets) {} 2728 else version(custom_widgets) { 2729 enum scrollClickRepeatInterval = 50; 2730 2731 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2732 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2733 enum activeTabColor = lightAccentColor; 2734 enum hoveringColor = Color(228, 228, 228); 2735 enum buttonColor = windowBackgroundColor; 2736 enum depressedButtonColor = darkAccentColor; 2737 enum activeListXorColor = Color(255, 255, 127); 2738 enum progressBarColor = Color(0, 0, 128); 2739 enum activeMenuItemColor = Color(0, 0, 128); 2740 2741 }} 2742 else static assert(false); 2743 deprecated("Get these properties off the `visualTheme` instead.") { 2744 // these are used by horizontal rule so not just custom_widgets. for now at least. 2745 enum darkAccentColor = Color(172, 172, 172); 2746 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2747 } 2748 2749 private const(wchar)* toWstringzInternal(in char[] s) { 2750 wchar[] str; 2751 str.reserve(s.length + 1); 2752 foreach(dchar ch; s) 2753 str ~= ch; 2754 str ~= '\0'; 2755 return str.ptr; 2756 } 2757 2758 static if(SimpledisplayTimerAvailable) 2759 void setClickRepeat(Widget w, int interval, int delay = 250) { 2760 Timer timer; 2761 int delayRemaining = delay / interval; 2762 if(delayRemaining <= 1) 2763 delayRemaining = 2; 2764 2765 immutable originalDelayRemaining = delayRemaining; 2766 2767 w.addDirectEventListener((scope MouseDownEvent ev) { 2768 if(ev.srcElement !is w) 2769 return; 2770 if(timer !is null) { 2771 timer.destroy(); 2772 timer = null; 2773 } 2774 delayRemaining = originalDelayRemaining; 2775 timer = new Timer(interval, () { 2776 if(delayRemaining > 0) 2777 delayRemaining--; 2778 else { 2779 auto ev = new Event("triggered", w); 2780 ev.sendDirectly(); 2781 } 2782 }); 2783 }); 2784 2785 w.addDirectEventListener((scope MouseUpEvent ev) { 2786 if(ev.srcElement !is w) 2787 return; 2788 if(timer !is null) { 2789 timer.destroy(); 2790 timer = null; 2791 } 2792 }); 2793 2794 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2795 if(ev.srcElement !is w) 2796 return; 2797 if(timer !is null) { 2798 timer.destroy(); 2799 timer = null; 2800 } 2801 }); 2802 2803 } 2804 else 2805 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2806 2807 enum FrameStyle { 2808 none, /// 2809 risen, /// a 3d pop-out effect (think Windows 95 button) 2810 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2811 solid, /// 2812 dotted, /// 2813 fantasy, /// a style based on a popular fantasy video game 2814 } 2815 2816 version(custom_widgets) 2817 deprecated 2818 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2819 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2820 } 2821 2822 version(custom_widgets) 2823 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2824 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2825 } 2826 2827 version(custom_widgets) 2828 deprecated 2829 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2830 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2831 } 2832 2833 int getBorderWidth(FrameStyle style) { 2834 final switch(style) { 2835 case FrameStyle.sunk, FrameStyle.risen: 2836 return 2; 2837 case FrameStyle.none: 2838 return 0; 2839 case FrameStyle.solid: 2840 return 1; 2841 case FrameStyle.dotted: 2842 return 1; 2843 case FrameStyle.fantasy: 2844 return 3; 2845 } 2846 } 2847 2848 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2849 int borderWidth = getBorderWidth(style); 2850 final switch(style) { 2851 case FrameStyle.sunk, FrameStyle.risen: 2852 // outer layer 2853 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2854 break; 2855 case FrameStyle.none: 2856 painter.outlineColor = background; 2857 break; 2858 case FrameStyle.solid: 2859 painter.pen = Pen(border, 1); 2860 break; 2861 case FrameStyle.dotted: 2862 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2863 break; 2864 case FrameStyle.fantasy: 2865 painter.pen = Pen(border, 3); 2866 break; 2867 } 2868 2869 painter.fillColor = background; 2870 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2871 2872 2873 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2874 // 3d effect 2875 auto vt = WidgetPainter.visualTheme; 2876 2877 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2878 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2879 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2880 2881 // inner layer 2882 //right, bottom 2883 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2884 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2885 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2886 // left, top 2887 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2888 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2889 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2890 } else if(style == FrameStyle.fantasy) { 2891 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2892 painter.fillColor = Color.transparent; 2893 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2894 } 2895 2896 return borderWidth; 2897 } 2898 2899 /++ 2900 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. 2901 2902 See_Also: 2903 [MenuItem] 2904 [ToolButton] 2905 [Menu.addItem] 2906 +/ 2907 class Action { 2908 version(win32_widgets) { 2909 private int id; 2910 private static int lastId = 9000; 2911 private static Action[int] mapping; 2912 } 2913 2914 KeyEvent accelerator; 2915 2916 // FIXME: disable message 2917 // and toggle thing? 2918 // ??? and trigger arguments too ??? 2919 2920 /++ 2921 Params: 2922 label = the textual label 2923 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2924 triggered = initial handler, more can be added via the [triggered] member. 2925 +/ 2926 this(string label, ushort icon = 0, void delegate() triggered = null) { 2927 this.label = label; 2928 this.iconId = icon; 2929 if(triggered !is null) 2930 this.triggered ~= triggered; 2931 version(win32_widgets) { 2932 id = ++lastId; 2933 mapping[id] = this; 2934 } 2935 } 2936 2937 private string label; 2938 private ushort iconId; 2939 // icon 2940 2941 // when it is triggered, the triggered event is fired on the window 2942 /// The list of handlers when it is triggered. 2943 void delegate()[] triggered; 2944 } 2945 2946 /* 2947 plan: 2948 keyboard accelerators 2949 2950 * menus (and popups and tooltips) 2951 * status bar 2952 * toolbars and buttons 2953 2954 sortable table view 2955 2956 maybe notification area icons 2957 basic clipboard 2958 2959 * radio box 2960 splitter 2961 toggle buttons (optionally mutually exclusive, like in Paint) 2962 label, rich text display, multi line plain text (selectable) 2963 * fieldset 2964 * nestable grid layout 2965 single line text input 2966 * multi line text input 2967 slider 2968 spinner 2969 list box 2970 drop down 2971 combo box 2972 auto complete box 2973 * progress bar 2974 2975 terminal window/widget (on unix it might even be a pty but really idk) 2976 2977 ok button 2978 cancel button 2979 2980 keyboard hotkeys 2981 2982 scroll widget 2983 2984 event redirections and network transparency 2985 script integration 2986 */ 2987 2988 2989 /* 2990 MENUS 2991 2992 auto bar = new MenuBar(window); 2993 window.menuBar = bar; 2994 2995 auto fileMenu = bar.addItem(new Menu("&File")); 2996 fileMenu.addItem(new MenuItem("&Exit")); 2997 2998 2999 EVENTS 3000 3001 For controls, you should usually use "triggered" rather than "click", etc., because 3002 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3003 This is the case on menus and pushbuttons. 3004 3005 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3006 */ 3007 3008 3009 /* 3010 enum LinePreference { 3011 AlwaysOnOwnLine, // always on its own line 3012 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3013 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 3014 } 3015 */ 3016 3017 /++ 3018 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. 3019 3020 --- 3021 class MyWidget : Widget { 3022 this(Widget parent) { super(parent); } 3023 3024 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3025 mixin Padding!q{4}; 3026 3027 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3028 mixin Margin!q{8}; 3029 3030 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3031 // while Top/Bottom/Right remain 8 from the mixin above. 3032 override int marginLeft() { return 2; } 3033 } 3034 --- 3035 3036 3037 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]). 3038 3039 Padding is the area inside a widget where its background is drawn, but the content avoids. 3040 3041 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!). 3042 3043 * 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. 3044 +/ 3045 mixin template Padding(string code) { 3046 override int paddingLeft() { return mixin(code);} 3047 override int paddingRight() { return mixin(code);} 3048 override int paddingTop() { return mixin(code);} 3049 override int paddingBottom() { return mixin(code);} 3050 } 3051 3052 /// ditto 3053 mixin template Margin(string code) { 3054 override int marginLeft() { return mixin(code);} 3055 override int marginRight() { return mixin(code);} 3056 override int marginTop() { return mixin(code);} 3057 override int marginBottom() { return mixin(code);} 3058 } 3059 3060 private 3061 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3062 enum calcingV = relevantMeasure == "height"; 3063 3064 parent.registerMovement(); 3065 3066 if(parent.children.length == 0) 3067 return; 3068 3069 auto parentStyle = parent.getComputedStyle(); 3070 3071 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3072 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3073 3074 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3075 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3076 3077 // my own width and height should already be set by the caller of this function... 3078 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3079 mixin("parentStyle.padding"~firstThingy~"()") - 3080 mixin("parentStyle.padding"~secondThingy~"()"); 3081 3082 int stretchinessSum; 3083 int stretchyChildSum; 3084 int lastMargin = 0; 3085 3086 int shrinkinessSum; 3087 int shrinkyChildSum; 3088 3089 // set initial size 3090 foreach(child; parent.children) { 3091 3092 auto childStyle = child.getComputedStyle(); 3093 3094 if(cast(StaticPosition) child) 3095 continue; 3096 if(child.hidden) 3097 continue; 3098 3099 const iw = child.flexBasisWidth(); 3100 const ih = child.flexBasisHeight(); 3101 3102 static if(calcingV) { 3103 child.width = parent.width - 3104 mixin("childStyle.margin"~otherFirstThingy~"()") - 3105 mixin("childStyle.margin"~otherSecondThingy~"()") - 3106 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3107 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3108 3109 if(child.width < 0) 3110 child.width = 0; 3111 if(child.width > childStyle.maxWidth()) 3112 child.width = childStyle.maxWidth(); 3113 3114 if(iw > 0) { 3115 auto totalPossible = child.width; 3116 if(child.width > iw && child.widthStretchiness() == 0) 3117 child.width = iw; 3118 } 3119 3120 child.height = mymax(childStyle.minHeight(), ih); 3121 } else { 3122 // set to take all the space 3123 child.height = parent.height - 3124 mixin("childStyle.margin"~firstThingy~"()") - 3125 mixin("childStyle.margin"~secondThingy~"()") - 3126 mixin("parentStyle.padding"~firstThingy~"()") - 3127 mixin("parentStyle.padding"~secondThingy~"()"); 3128 3129 // then clamp it 3130 if(child.height < 0) 3131 child.height = 0; 3132 if(child.height > childStyle.maxHeight()) 3133 child.height = childStyle.maxHeight(); 3134 3135 // and if possible, respect the ideal target 3136 if(ih > 0) { 3137 auto totalPossible = child.height; 3138 if(child.height > ih && child.heightStretchiness() == 0) 3139 child.height = ih; 3140 } 3141 3142 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3143 child.width = mymax(childStyle.minWidth(), iw); 3144 } 3145 3146 spaceRemaining -= mixin("child." ~ relevantMeasure); 3147 3148 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3149 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3150 lastMargin = margin; 3151 spaceRemaining -= thisMargin + margin; 3152 3153 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3154 stretchinessSum += s; 3155 if(s > 0) 3156 stretchyChildSum++; 3157 3158 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3159 shrinkinessSum += s2; 3160 if(s2 > 0) 3161 shrinkyChildSum++; 3162 } 3163 3164 if(spaceRemaining < 0 && shrinkyChildSum) { 3165 // shrink to get into the space if it is possible 3166 auto toRemove = -spaceRemaining; 3167 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3168 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3169 3170 // FIXME: wtf why am i shrinking things with no shrinkiness? 3171 3172 foreach(child; parent.children) { 3173 auto childStyle = child.getComputedStyle(); 3174 if(cast(StaticPosition) child) 3175 continue; 3176 if(child.hidden) 3177 continue; 3178 static if(calcingV) { 3179 auto maximum = childStyle.maxHeight(); 3180 } else { 3181 auto maximum = childStyle.maxWidth(); 3182 } 3183 3184 if(mixin("child._" ~ relevantMeasure) >= maximum) 3185 continue; 3186 3187 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3188 3189 spaceRemaining += removalPerItem + remainder; 3190 } 3191 } 3192 3193 // stretch to fill space 3194 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3195 auto spacePerChild = spaceRemaining / stretchinessSum; 3196 bool spreadEvenly; 3197 bool giveToBiggest; 3198 if(spacePerChild <= 0) { 3199 spacePerChild = spaceRemaining / stretchyChildSum; 3200 spreadEvenly = true; 3201 } 3202 if(spacePerChild <= 0) { 3203 giveToBiggest = true; 3204 } 3205 int previousSpaceRemaining = spaceRemaining; 3206 stretchinessSum = 0; 3207 Widget mostStretchy; 3208 int mostStretchyS; 3209 foreach(child; parent.children) { 3210 auto childStyle = child.getComputedStyle(); 3211 if(cast(StaticPosition) child) 3212 continue; 3213 if(child.hidden) 3214 continue; 3215 static if(calcingV) { 3216 auto maximum = childStyle.maxHeight(); 3217 } else { 3218 auto maximum = childStyle.maxWidth(); 3219 } 3220 3221 if(mixin("child." ~ relevantMeasure) >= maximum) { 3222 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3223 mixin("child._" ~ relevantMeasure) -= adj; 3224 spaceRemaining += adj; 3225 continue; 3226 } 3227 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3228 if(s <= 0) 3229 continue; 3230 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3231 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3232 spaceRemaining -= spaceAdjustment; 3233 if(mixin("child." ~ relevantMeasure) > maximum) { 3234 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3235 mixin("child._" ~ relevantMeasure) -= diff; 3236 spaceRemaining += diff; 3237 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3238 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3239 if(mostStretchy is null || s >= mostStretchyS) { 3240 mostStretchy = child; 3241 mostStretchyS = s; 3242 } 3243 } 3244 } 3245 3246 if(giveToBiggest && mostStretchy !is null) { 3247 auto child = mostStretchy; 3248 auto childStyle = child.getComputedStyle(); 3249 int spaceAdjustment = spaceRemaining; 3250 3251 static if(calcingV) 3252 auto maximum = childStyle.maxHeight(); 3253 else 3254 auto maximum = childStyle.maxWidth(); 3255 3256 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3257 spaceRemaining -= spaceAdjustment; 3258 if(mixin("child._" ~ relevantMeasure) > maximum) { 3259 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3260 mixin("child._" ~ relevantMeasure) -= diff; 3261 spaceRemaining += diff; 3262 } 3263 } 3264 3265 if(spaceRemaining == previousSpaceRemaining) { 3266 if(mostStretchy !is null) { 3267 static if(calcingV) 3268 auto maximum = mostStretchy.maxHeight(); 3269 else 3270 auto maximum = mostStretchy.maxWidth(); 3271 3272 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3273 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3274 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3275 } 3276 break; // apparently nothing more we can do 3277 } 3278 } 3279 3280 foreach(child; parent.children) { 3281 auto childStyle = child.getComputedStyle(); 3282 if(cast(StaticPosition) child) 3283 continue; 3284 if(child.hidden) 3285 continue; 3286 3287 static if(calcingV) 3288 auto maximum = childStyle.maxHeight(); 3289 else 3290 auto maximum = childStyle.maxWidth(); 3291 if(mixin("child._" ~ relevantMeasure) > maximum) 3292 mixin("child._" ~ relevantMeasure) = maximum; 3293 } 3294 3295 // position 3296 lastMargin = 0; 3297 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3298 foreach(child; parent.children) { 3299 auto childStyle = child.getComputedStyle(); 3300 if(cast(StaticPosition) child) { 3301 child.recomputeChildLayout(); 3302 continue; 3303 } 3304 if(child.hidden) 3305 continue; 3306 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3307 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3308 currentPos += thisMargin; 3309 static if(calcingV) { 3310 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3311 child.y = currentPos; 3312 } else { 3313 child.x = currentPos; 3314 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3315 3316 } 3317 currentPos += mixin("child." ~ relevantMeasure); 3318 currentPos += margin; 3319 lastMargin = margin; 3320 3321 child.recomputeChildLayout(); 3322 } 3323 } 3324 3325 int mymax(int a, int b) { return a > b ? a : b; } 3326 int mymax(int a, int b, int c) { 3327 auto d = mymax(a, b); 3328 return c > d ? c : d; 3329 } 3330 3331 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3332 // and here, it must be integrable with the layout, the event system, and not be painted over. 3333 version(win32_widgets) { 3334 3335 // this function just does stuff that a parent window needs for redirection 3336 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3337 this_.hookedWndProc(msg, wParam, lParam); 3338 3339 switch(msg) { 3340 3341 case WM_VSCROLL, WM_HSCROLL: 3342 auto pos = HIWORD(wParam); 3343 auto m = LOWORD(wParam); 3344 3345 auto scrollbarHwnd = cast(HWND) lParam; 3346 3347 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3348 3349 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3350 3351 switch(m) { 3352 /+ 3353 // I don't think those messages are ever actually sent normally by the widget itself, 3354 // they are more used for the keyboard interface. methinks. 3355 case SB_BOTTOM: 3356 // writeln("end"); 3357 auto event = new Event("scrolltoend", *widgetp); 3358 event.dispatch(); 3359 //if(!event.defaultPrevented) 3360 break; 3361 case SB_TOP: 3362 // writeln("top"); 3363 auto event = new Event("scrolltobeginning", *widgetp); 3364 event.dispatch(); 3365 break; 3366 case SB_ENDSCROLL: 3367 // idk 3368 break; 3369 +/ 3370 case SB_LINEDOWN: 3371 (*widgetp).emitCommand!"scrolltonextline"(); 3372 return 0; 3373 case SB_LINEUP: 3374 (*widgetp).emitCommand!"scrolltopreviousline"(); 3375 return 0; 3376 case SB_PAGEDOWN: 3377 (*widgetp).emitCommand!"scrolltonextpage"(); 3378 return 0; 3379 case SB_PAGEUP: 3380 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3381 return 0; 3382 case SB_THUMBPOSITION: 3383 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3384 ev.dispatch(); 3385 return 0; 3386 case SB_THUMBTRACK: 3387 // eh kinda lying but i like the real time update display 3388 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3389 ev.dispatch(); 3390 3391 // the event loop doesn't seem to carry on with a requested redraw.. 3392 // so we request it to get our dirty bit set... 3393 // then we need to immediately actually redraw it too for instant feedback to user 3394 SimpleWindow.processAllCustomEvents(); 3395 SimpleWindow.processAllCustomEvents(); 3396 //if(this_.parentWindow) 3397 //this_.parentWindow.actualRedraw(); 3398 3399 // and this ensures the WM_PAINT message is sent fairly quickly 3400 // still seems to lag a little in large windows but meh it basically works. 3401 if(this_.parentWindow) { 3402 // FIXME: if painting is slow, this does still lag 3403 // we probably will want to expose some user hook to ScrollWindowEx 3404 // or something. 3405 UpdateWindow(this_.parentWindow.hwnd); 3406 } 3407 return 0; 3408 default: 3409 } 3410 } 3411 break; 3412 3413 case WM_CONTEXTMENU: 3414 auto hwndFrom = cast(HWND) wParam; 3415 3416 auto xPos = cast(short) LOWORD(lParam); 3417 auto yPos = cast(short) HIWORD(lParam); 3418 3419 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3420 POINT p; 3421 p.x = xPos; 3422 p.y = yPos; 3423 ScreenToClient(hwnd, &p); 3424 auto clientX = cast(ushort) p.x; 3425 auto clientY = cast(ushort) p.y; 3426 3427 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3428 3429 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3430 return 0; 3431 } 3432 } 3433 break; 3434 3435 case WM_DRAWITEM: 3436 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3437 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3438 return (*widgetp).handleWmDrawItem(dis); 3439 } 3440 break; 3441 3442 case WM_NOTIFY: 3443 auto hdr = cast(NMHDR*) lParam; 3444 auto hwndFrom = hdr.hwndFrom; 3445 auto code = hdr.code; 3446 3447 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3448 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3449 } 3450 break; 3451 case WM_COMMAND: 3452 auto handle = cast(HWND) lParam; 3453 auto cmd = HIWORD(wParam); 3454 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3455 3456 default: 3457 // pass it on 3458 } 3459 return 0; 3460 } 3461 3462 3463 3464 extern(Windows) 3465 private 3466 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3467 // but can i merge them?! 3468 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3469 // try { writeln(iMessage); } catch(Exception e) {}; 3470 3471 if(auto te = hWnd in Widget.nativeMapping) { 3472 try { 3473 3474 te.hookedWndProc(iMessage, wParam, lParam); 3475 3476 int mustReturn; 3477 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3478 if(mustReturn) 3479 return ret; 3480 3481 if(iMessage == WM_SETFOCUS) { 3482 auto lol = *te; 3483 while(lol !is null && lol.implicitlyCreated) 3484 lol = lol.parent; 3485 lol.focus(); 3486 //(*te).parentWindow.focusedWidget = lol; 3487 } 3488 3489 3490 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3491 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3492 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3493 //GetStockObject(NULL_BRUSH); 3494 } 3495 3496 auto pos = getChildPositionRelativeToParentOrigin(*te); 3497 lastDefaultPrevented = false; 3498 // try { writeln(typeid(*te)); } catch(Exception e) {} 3499 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3500 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3501 else { 3502 // it was something we recognized, should only call the window procedure if the default was not prevented 3503 } 3504 } catch(Exception e) { 3505 assert(0, e.toString()); 3506 } 3507 return 0; 3508 } 3509 assert(0, "shouldn't be receiving messages for this window...."); 3510 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3511 } 3512 3513 extern(Windows) 3514 private 3515 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3516 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3517 if(iMessage == WM_ERASEBKGND) { 3518 auto dc = GetDC(hWnd); 3519 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3520 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3521 RECT r; 3522 GetWindowRect(hWnd, &r); 3523 // since the pen is null, to fill the whole space, we need the +1 on both. 3524 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3525 SelectObject(dc, p); 3526 SelectObject(dc, b); 3527 ReleaseDC(hWnd, dc); 3528 InvalidateRect(hWnd, null, false); // redraw the border 3529 return 1; 3530 } 3531 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3532 } 3533 3534 /++ 3535 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3536 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3537 of minigui's expectations. 3538 3539 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3540 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3541 3542 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3543 3544 To check if you can use this, use `static if(UsingWin32Widgets)`. 3545 +/ 3546 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3547 assert(p.parentWindow !is null); 3548 assert(p.parentWindow.win.impl.hwnd !is null); 3549 3550 auto bsgroupbox = style == BS_GROUPBOX; 3551 3552 HWND phwnd; 3553 3554 auto wtf = p.parent; 3555 while(wtf) { 3556 if(wtf.hwnd !is null) { 3557 phwnd = wtf.hwnd; 3558 break; 3559 } 3560 wtf = wtf.parent; 3561 } 3562 3563 if(phwnd is null) 3564 phwnd = p.parentWindow.win.impl.hwnd; 3565 3566 assert(phwnd !is null); 3567 3568 WCharzBuffer wt = WCharzBuffer(windowText); 3569 3570 style |= WS_VISIBLE | WS_CHILD; 3571 //if(className != WC_TABCONTROL) 3572 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3573 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3574 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3575 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3576 3577 assert(p.hwnd !is null); 3578 3579 3580 static HFONT font; 3581 if(font is null) { 3582 NONCLIENTMETRICS params; 3583 params.cbSize = params.sizeof; 3584 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3585 font = CreateFontIndirect(¶ms.lfMessageFont); 3586 } 3587 } 3588 3589 if(font) 3590 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3591 3592 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3593 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3594 Widget.nativeMapping[p.hwnd] = p; 3595 3596 if(bsgroupbox) 3597 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3598 else 3599 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3600 3601 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3602 3603 p.registerMovement(); 3604 } 3605 } 3606 3607 version(win32_widgets) 3608 private 3609 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3610 if(hwnd is null || hwnd in Widget.nativeMapping) 3611 return true; 3612 auto parent = cast(Widget) cast(void*) lparam; 3613 Widget p = new Widget(null); 3614 p._parent = parent; 3615 p.parentWindow = parent.parentWindow; 3616 p.hwnd = hwnd; 3617 p.implicitlyCreated = true; 3618 Widget.nativeMapping[p.hwnd] = p; 3619 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3620 return true; 3621 } 3622 3623 /++ 3624 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3625 +/ 3626 struct WidgetPainter { 3627 this(ScreenPainter screenPainter, Widget drawingUpon) { 3628 this.drawingUpon = drawingUpon; 3629 this.screenPainter = screenPainter; 3630 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3631 this.screenPainter.setFont(font); 3632 } 3633 3634 /++ 3635 EXPERIMENTAL. subject to change. 3636 3637 When you draw a cursor, you can draw this to notify your window of where it is, 3638 for IME systems to use. 3639 +/ 3640 void notifyCursorPosition(int x, int y, int width, int height) { 3641 if(auto a = drawingUpon.parentWindow) 3642 if(auto w = a.inputProxy) { 3643 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3644 } 3645 } 3646 3647 3648 /// 3649 ScreenPainter screenPainter; 3650 /// Forward to the screen painter for other methods 3651 alias screenPainter this; 3652 3653 private Widget drawingUpon; 3654 3655 /++ 3656 This is the list of rectangles that actually need to be redrawn. 3657 3658 Not actually implemented yet. 3659 +/ 3660 Rectangle[] invalidatedRectangles; 3661 3662 private static BaseVisualTheme _visualTheme; 3663 3664 /++ 3665 Functions to access the visual theme and helpers to easily use it. 3666 3667 These are aware of the current widget's computed style out of the theme. 3668 +/ 3669 static @property BaseVisualTheme visualTheme() { 3670 if(_visualTheme is null) 3671 _visualTheme = new DefaultVisualTheme(); 3672 return _visualTheme; 3673 } 3674 3675 /// ditto 3676 static @property void visualTheme(BaseVisualTheme theme) { 3677 _visualTheme = theme; 3678 3679 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 3680 } 3681 3682 /// ditto 3683 Color themeForeground() { 3684 return drawingUpon.getComputedStyle().foregroundColor(); 3685 } 3686 3687 /// ditto 3688 Color themeBackground() { 3689 return drawingUpon.getComputedStyle().background.color; 3690 } 3691 3692 int isDarkTheme() { 3693 return 0; // unspecified, yes, no as enum. FIXME 3694 } 3695 3696 /++ 3697 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. 3698 3699 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3700 3701 If you change teh clip rectangle, you should change it back before you return. 3702 3703 3704 The sequence it uses is: 3705 background 3706 content (delegated to you) 3707 border 3708 focused outline 3709 selected overlay 3710 3711 Example code: 3712 3713 --- 3714 void paint(WidgetPainter painter) { 3715 painter.drawThemed((bounds) { 3716 return bounds; // if the selection overlay should be contained, you can return it here. 3717 }); 3718 } 3719 --- 3720 +/ 3721 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3722 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3723 return drawBody(bounds); 3724 }); 3725 } 3726 // this overload is actually mroe for setting the delegate to a virtual function 3727 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3728 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3729 3730 auto cs = drawingUpon.getComputedStyle(); 3731 3732 auto bg = cs.background.color; 3733 3734 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3735 3736 rect.left += borderWidth; 3737 rect.right -= borderWidth; 3738 rect.top += borderWidth; 3739 rect.bottom -= borderWidth; 3740 3741 auto insideBorderRect = rect; 3742 3743 rect.left += cs.paddingLeft; 3744 rect.right -= cs.paddingRight; 3745 rect.top += cs.paddingTop; 3746 rect.bottom -= cs.paddingBottom; 3747 3748 this.outlineColor = this.themeForeground; 3749 this.fillColor = bg; 3750 3751 auto widgetFont = cs.fontCached; 3752 if(widgetFont !is null) 3753 this.setFont(widgetFont); 3754 3755 rect = drawBody(this, rect); 3756 3757 if(widgetFont !is null) { 3758 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3759 this.setFont(vtFont); 3760 else 3761 this.setFont(null); 3762 } 3763 3764 if(auto os = cs.outlineStyle()) { 3765 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3766 this.fillColor = Color.transparent; 3767 this.drawRectangle(insideBorderRect); 3768 } 3769 } 3770 3771 /++ 3772 First, draw the background. 3773 Then draw your content. 3774 Next, draw the border. 3775 And the focused indicator. 3776 And the is-selected box. 3777 3778 If it is focused i can draw the outline too... 3779 3780 If selected i can even do the xor action but that's at the end. 3781 +/ 3782 void drawThemeBackground() { 3783 3784 } 3785 3786 void drawThemeBorder() { 3787 3788 } 3789 3790 // all this stuff is a dangerous experiment.... 3791 static class ScriptableVersion { 3792 ScreenPainterImplementation* p; 3793 int originX, originY; 3794 3795 @scriptable: 3796 void drawRectangle(int x, int y, int width, int height) { 3797 p.drawRectangle(x + originX, y + originY, width, height); 3798 } 3799 void drawLine(int x1, int y1, int x2, int y2) { 3800 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3801 } 3802 void drawText(int x, int y, string text) { 3803 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3804 } 3805 void setOutlineColor(int r, int g, int b) { 3806 p.pen = Pen(Color(r,g,b), 1); 3807 } 3808 void setFillColor(int r, int g, int b) { 3809 p.fillColor = Color(r,g,b); 3810 } 3811 } 3812 3813 ScriptableVersion toArsdJsvar() { 3814 auto sv = new ScriptableVersion; 3815 sv.p = this.screenPainter.impl; 3816 sv.originX = this.screenPainter.originX; 3817 sv.originY = this.screenPainter.originY; 3818 return sv; 3819 } 3820 3821 static WidgetPainter fromJsVar(T)(T t) { 3822 return WidgetPainter.init; 3823 } 3824 // done.......... 3825 } 3826 3827 3828 struct Style { 3829 static struct helper(string m, T) { 3830 enum method = m; 3831 T v; 3832 3833 mixin template MethodOverride(typeof(this) v) { 3834 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3835 } 3836 } 3837 3838 static auto opDispatch(string method, T)(T value) { 3839 return helper!(method, T)(value); 3840 } 3841 } 3842 3843 /++ 3844 Implementation detail of the [ControlledBy] UDA. 3845 3846 History: 3847 Added Oct 28, 2020 3848 +/ 3849 struct ControlledBy_(T, Args...) { 3850 Args args; 3851 3852 static if(Args.length) 3853 this(Args args) { 3854 this.args = args; 3855 } 3856 3857 private T construct(Widget parent) { 3858 return new T(args, parent); 3859 } 3860 } 3861 3862 /++ 3863 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3864 3865 History: 3866 Added Oct 28, 2020 3867 +/ 3868 auto ControlledBy(T, Args...)(Args args) { 3869 return ControlledBy_!(T, Args)(args); 3870 } 3871 3872 struct ContainerMeta { 3873 string name; 3874 ContainerMeta[] children; 3875 Widget function(Widget parent) factory; 3876 3877 Widget instantiate(Widget parent) { 3878 auto n = factory(parent); 3879 n.name = name; 3880 foreach(child; children) 3881 child.instantiate(n); 3882 return n; 3883 } 3884 } 3885 3886 /++ 3887 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3888 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3889 3890 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3891 structures. It works fine on structs declared inside functions though. 3892 3893 See: https://issues.dlang.org/show_bug.cgi?id=21984 3894 +/ 3895 template Container(CArgs...) { 3896 static if(CArgs.length && is(CArgs[0] : Widget)) { 3897 private alias Super = CArgs[0]; 3898 private alias CArgs2 = CArgs[1 .. $]; 3899 } else { 3900 private alias Super = Layout; 3901 private alias CArgs2 = CArgs; 3902 } 3903 3904 class Container : Super { 3905 this(Widget parent) { super(parent); } 3906 3907 // just to partially support old gdc versions 3908 version(GNU) { 3909 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3910 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3911 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3912 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3913 } else mixin(q{ 3914 static foreach(Arg; CArgs2) { 3915 mixin Arg.MethodOverride!(Arg); 3916 } 3917 }); 3918 3919 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3920 return ContainerMeta( 3921 name, 3922 children.dup, 3923 function (Widget parent) { return new typeof(this)(parent); } 3924 ); 3925 } 3926 3927 static ContainerMeta opCall(ContainerMeta[] children...) { 3928 return opCall(null, children); 3929 } 3930 } 3931 } 3932 3933 /++ 3934 The data controller widget is created by reflecting over the given 3935 data type. You can use [ControlledBy] as a UDA on a struct or 3936 just let it create things automatically. 3937 3938 Unlike [dialog], this uses real-time updating of the data and 3939 you add it to another window yourself. 3940 3941 --- 3942 struct Test { 3943 int x; 3944 int y; 3945 } 3946 3947 auto window = new Window(); 3948 auto dcw = new DataControllerWidget!Test(new Test, window); 3949 --- 3950 3951 The way it works is any public members are given a widget based 3952 on their data type, and public methods trigger an action button 3953 if no relevant parameters or a dialog action if it does have 3954 parameters, similar to the [menu] facility. 3955 3956 If you change data programmatically, without going through the 3957 DataControllerWidget methods, you will have to tell it something 3958 has changed and it needs to redraw. This is done with the `invalidate` 3959 method. 3960 3961 History: 3962 Added Oct 28, 2020 3963 +/ 3964 /// Group: generating_from_code 3965 class DataControllerWidget(T) : WidgetContainer { 3966 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3967 private alias Tref = T; 3968 else 3969 private alias Tref = T*; 3970 3971 Tref datum; 3972 3973 /++ 3974 See_also: [addDataControllerWidget] 3975 +/ 3976 this(Tref datum, Widget parent) { 3977 this.datum = datum; 3978 3979 Widget cp = this; 3980 3981 super(parent); 3982 3983 foreach(attr; __traits(getAttributes, T)) 3984 static if(is(typeof(attr) == ContainerMeta)) { 3985 cp = attr.instantiate(this); 3986 } 3987 3988 auto def = this.getByName("default"); 3989 if(def !is null) 3990 cp = def; 3991 3992 Widget helper(string name) { 3993 auto maybe = this.getByName(name); 3994 if(maybe is null) 3995 return cp; 3996 return maybe; 3997 3998 } 3999 4000 foreach(member; __traits(allMembers, T)) 4001 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4002 static if(is(typeof(__traits(getMember, this.datum, member)))) 4003 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4004 void delegate() update; 4005 4006 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4007 4008 if(update) 4009 updaters ~= update; 4010 4011 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4012 w.addEventListener("triggered", delegate() { 4013 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 4014 notifyDataUpdated(); 4015 }); 4016 } else static if(is(typeof(w.isChecked) == bool)) { 4017 w.addEventListener(EventType.change, (Event ev) { 4018 __traits(getMember, this.datum, member) = w.isChecked; 4019 }); 4020 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4021 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4022 } else static if(is(typeof(w.value) == int)) { 4023 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4024 } else static if(is(typeof(w) == DropDownSelection)) { 4025 // special case for this to kinda support enums and such. coudl be better though 4026 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4027 } else { 4028 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4029 } 4030 } 4031 } 4032 4033 /++ 4034 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4035 4036 History: 4037 Added May 28, 2021 4038 +/ 4039 void notifyDataUpdated() { 4040 foreach(updater; updaters) 4041 updater(); 4042 4043 this.emit!(ChangeEvent!void)(delegate{}); 4044 } 4045 4046 private Widget[string] memberWidgets; 4047 private void delegate()[] updaters; 4048 4049 mixin Emits!(ChangeEvent!void); 4050 } 4051 4052 private int saturatedSum(int[] values...) { 4053 int sum; 4054 foreach(value; values) { 4055 if(value == int.max) 4056 return int.max; 4057 sum += value; 4058 } 4059 return sum; 4060 } 4061 4062 void genericSetValue(T, W)(T* where, W what) { 4063 import std.conv; 4064 *where = to!T(what); 4065 //*where = cast(T) stringToLong(what); 4066 } 4067 4068 /++ 4069 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4070 4071 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4072 4073 Note that this creates the widget but does not attach any event handlers to it. 4074 +/ 4075 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4076 4077 string displayName = __traits(identifier, tt).beautify; 4078 4079 static if(controlledByCount!tt == 1) { 4080 foreach(i, attr; __traits(getAttributes, tt)) { 4081 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4082 auto w = attr.construct(parent); 4083 static if(__traits(compiles, w.setPosition(*valptr))) 4084 update = () { w.setPosition(*valptr); }; 4085 else static if(__traits(compiles, w.setValue(*valptr))) 4086 update = () { w.setValue(*valptr); }; 4087 4088 if(update) 4089 update(); 4090 return w; 4091 } 4092 } 4093 } else static if(controlledByCount!tt == 0) { 4094 static if(is(typeof(tt) == enum)) { 4095 // FIXME: update 4096 auto dds = new DropDownSelection(parent); 4097 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4098 dds.addOption(option); 4099 if(__traits(getMember, typeof(tt), option) == *valptr) 4100 dds.setSelection(cast(int) idx); 4101 } 4102 return dds; 4103 } else static if(is(typeof(tt) == bool)) { 4104 auto box = new Checkbox(displayName, parent); 4105 update = () { box.isChecked = *valptr; }; 4106 update(); 4107 return box; 4108 } else static if(is(typeof(tt) : const long)) { 4109 auto le = new LabeledLineEdit(displayName, parent); 4110 update = () { le.content = toInternal!string(*valptr); }; 4111 update(); 4112 return le; 4113 } else static if(is(typeof(tt) : const double)) { 4114 auto le = new LabeledLineEdit(displayName, parent); 4115 import std.conv; 4116 update = () { le.content = to!string(*valptr); }; 4117 update(); 4118 return le; 4119 } else static if(is(typeof(tt) : const string)) { 4120 auto le = new LabeledLineEdit(displayName, parent); 4121 update = () { le.content = *valptr; }; 4122 update(); 4123 return le; 4124 } else static if(is(typeof(tt) == function)) { 4125 auto w = new Button(displayName, parent); 4126 return w; 4127 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4128 return parent.addDataControllerWidget(tt); 4129 } else static assert(0, typeof(tt).stringof); 4130 } else static assert(0, "multiple controllers not yet supported"); 4131 } 4132 4133 private template controlledByCount(alias tt) { 4134 static int helper() { 4135 int count; 4136 foreach(i, attr; __traits(getAttributes, tt)) 4137 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4138 count++; 4139 return count; 4140 } 4141 4142 enum controlledByCount = helper; 4143 } 4144 4145 /++ 4146 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4147 4148 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4149 4150 History: 4151 The `redrawOnChange` parameter was added on May 28, 2021. 4152 +/ 4153 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4154 auto dcw = new DataControllerWidget!T(t, parent); 4155 initializeDataControllerWidget(dcw, redrawOnChange); 4156 return dcw; 4157 } 4158 4159 /// ditto 4160 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4161 auto dcw = new DataControllerWidget!T(t, parent); 4162 initializeDataControllerWidget(dcw, redrawOnChange); 4163 return dcw; 4164 } 4165 4166 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4167 if(redrawOnChange !is null) 4168 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4169 } 4170 4171 /++ 4172 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. 4173 4174 History: 4175 Finalized on June 3, 2021 for the dub v10.0 release 4176 +/ 4177 struct StyleInformation { 4178 private Widget w; 4179 private BaseVisualTheme visualTheme; 4180 4181 private this(Widget w) { 4182 this.w = w; 4183 this.visualTheme = WidgetPainter.visualTheme; 4184 } 4185 4186 /++ 4187 Forwards to [Widget.Style] 4188 4189 Bugs: 4190 It is supposed to fall back to the [VisualTheme] if 4191 the style doesn't override the default, but that is 4192 not generally implemented. Many of them may end up 4193 being explicit overloads instead of the generic 4194 opDispatch fallback, like [font] is now. 4195 +/ 4196 public @property opDispatch(string name)() { 4197 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4198 w.useStyleProperties((scope Widget.Style props) { 4199 //visualTheme.useStyleProperties(w, (props) { 4200 prop = __traits(getMember, props, name); 4201 }); 4202 return prop; 4203 } 4204 4205 /++ 4206 Returns the cached font object associated with the widget, 4207 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4208 4209 History: 4210 Prior to March 21, 2022 (dub v10.7), `font` went through 4211 [opDispatch], which did not use the cache. You can now call it 4212 repeatedly without guilt. 4213 +/ 4214 public @property OperatingSystemFont font() { 4215 OperatingSystemFont prop; 4216 w.useStyleProperties((scope Widget.Style props) { 4217 prop = props.fontCached; 4218 }); 4219 if(prop is null) { 4220 prop = visualTheme.defaultFontCached(w.currentDpi); 4221 } 4222 return prop; 4223 } 4224 4225 @property { 4226 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4227 /** */ int paddingLeft() { return w.paddingLeft(); } 4228 /** */ int paddingRight() { return w.paddingRight(); } 4229 /** */ int paddingTop() { return w.paddingTop(); } 4230 /** */ int paddingBottom() { return w.paddingBottom(); } 4231 4232 /** */ int marginLeft() { return w.marginLeft(); } 4233 /** */ int marginRight() { return w.marginRight(); } 4234 /** */ int marginTop() { return w.marginTop(); } 4235 /** */ int marginBottom() { return w.marginBottom(); } 4236 4237 /** */ int maxHeight() { return w.maxHeight(); } 4238 /** */ int minHeight() { return w.minHeight(); } 4239 4240 /** */ int maxWidth() { return w.maxWidth(); } 4241 /** */ int minWidth() { return w.minWidth(); } 4242 4243 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4244 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4245 4246 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4247 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4248 4249 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4250 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4251 4252 // Global helpers some of these are unstable. 4253 static: 4254 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4255 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4256 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4257 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4258 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4259 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4260 4261 /** */ Color activeTabColor() { return lightAccentColor; } 4262 /** */ Color buttonColor() { return windowBackgroundColor; } 4263 /** */ Color depressedButtonColor() { return darkAccentColor; } 4264 /** */ Color hoveringColor() { return lightAccentColor; } 4265 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4266 auto c = WidgetPainter.visualTheme.selectionColor(); 4267 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4268 } 4269 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4270 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4271 } 4272 4273 4274 4275 /+ 4276 4277 private static auto extractStyleProperty(string name)(Widget w) { 4278 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4279 w.useStyleProperties((props) { 4280 prop = __traits(getMember, props, name); 4281 }); 4282 return prop; 4283 } 4284 4285 // FIXME: clear this upon a X server disconnect 4286 private static OperatingSystemFont[string] fontCache; 4287 4288 T getProperty(T)(string name, lazy T default_) { 4289 if(visualTheme !is null) { 4290 auto str = visualTheme.getPropertyString(w, name); 4291 if(str is null) 4292 return default_; 4293 static if(is(T == Color)) 4294 return Color.fromString(str); 4295 else static if(is(T == Measurement)) 4296 return Measurement(cast(int) toInternal!int(str)); 4297 else static if(is(T == WidgetBackground)) 4298 return WidgetBackground.fromString(str); 4299 else static if(is(T == OperatingSystemFont)) { 4300 if(auto f = str in fontCache) 4301 return *f; 4302 else 4303 return fontCache[str] = new OperatingSystemFont(str); 4304 } else static if(is(T == FrameStyle)) { 4305 switch(str) { 4306 default: 4307 return FrameStyle.none; 4308 foreach(style; __traits(allMembers, FrameStyle)) 4309 case style: 4310 return __traits(getMember, FrameStyle, style); 4311 } 4312 } else static assert(0); 4313 } else 4314 return default_; 4315 } 4316 4317 static struct Measurement { 4318 int value; 4319 alias value this; 4320 } 4321 4322 @property: 4323 4324 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4325 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4326 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4327 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4328 4329 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4330 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4331 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4332 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4333 4334 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4335 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4336 4337 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4338 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4339 4340 4341 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4342 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4343 4344 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4345 4346 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4347 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4348 4349 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4350 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4351 4352 4353 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4354 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4355 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4356 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4357 4358 Color activeTabColor() { return lightAccentColor; } 4359 Color buttonColor() { return windowBackgroundColor; } 4360 Color depressedButtonColor() { return darkAccentColor; } 4361 Color hoveringColor() { return Color(228, 228, 228); } 4362 Color activeListXorColor() { 4363 auto c = WidgetPainter.visualTheme.selectionColor(); 4364 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4365 } 4366 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4367 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4368 +/ 4369 } 4370 4371 4372 4373 // pragma(msg, __traits(classInstanceSize, Widget)); 4374 4375 /*private*/ template EventString(E) { 4376 static if(is(typeof(E.EventString))) 4377 enum EventString = E.EventString; 4378 else 4379 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4380 } 4381 4382 /*private*/ template EventStringIdentifier(E) { 4383 string helper() { 4384 auto es = EventString!E; 4385 char[] id = new char[](es.length * 2); 4386 size_t idx; 4387 foreach(char ch; es) { 4388 id[idx++] = cast(char)('a' + (ch >> 4)); 4389 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4390 } 4391 return cast(string) id; 4392 } 4393 4394 enum EventStringIdentifier = helper(); 4395 } 4396 4397 4398 template classStaticallyEmits(This, EventType) { 4399 static if(is(This Base == super)) 4400 static if(is(Base : Widget)) 4401 enum baseEmits = classStaticallyEmits!(Base, EventType); 4402 else 4403 enum baseEmits = false; 4404 else 4405 enum baseEmits = false; 4406 4407 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4408 4409 enum classStaticallyEmits = thisEmits || baseEmits; 4410 } 4411 4412 /++ 4413 A helper to make widgets out of other native windows. 4414 4415 History: 4416 Factored out of OpenGlWidget on November 5, 2021 4417 +/ 4418 class NestedChildWindowWidget : Widget { 4419 SimpleWindow win; 4420 4421 /++ 4422 Used on X to send focus to the appropriate child window when requested by the window manager. 4423 4424 Normally returns its own nested window. Can also return another child or null to revert to the parent 4425 if you override it in a child class. 4426 4427 History: 4428 Added April 2, 2022 (dub v10.8) 4429 +/ 4430 SimpleWindow focusableWindow() { 4431 return win; 4432 } 4433 4434 /// 4435 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4436 this(SimpleWindow win, Widget parent) { 4437 this.parentWindow = parent.parentWindow; 4438 this.win = win; 4439 4440 super(parent); 4441 windowsetup(win); 4442 } 4443 4444 static protected SimpleWindow getParentWindow(Widget parent) { 4445 assert(parent !is null); 4446 SimpleWindow pwin = parent.parentWindow.win; 4447 4448 version(win32_widgets) { 4449 HWND phwnd; 4450 auto wtf = parent; 4451 while(wtf) { 4452 if(wtf.hwnd) { 4453 phwnd = wtf.hwnd; 4454 break; 4455 } 4456 wtf = wtf.parent; 4457 } 4458 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4459 if(phwnd) 4460 pwin = new SimpleWindow(phwnd); 4461 } 4462 4463 return pwin; 4464 } 4465 4466 /++ 4467 Called upon the nested window being destroyed. 4468 Remember the window has already been destroyed at 4469 this point, so don't use the native handle for anything. 4470 4471 History: 4472 Added April 3, 2022 (dub v10.8) 4473 +/ 4474 protected void dispose() { 4475 4476 } 4477 4478 protected void windowsetup(SimpleWindow w) { 4479 /* 4480 win.onFocusChange = (bool getting) { 4481 if(getting) 4482 this.focus(); 4483 }; 4484 */ 4485 4486 /+ 4487 win.onFocusChange = (bool getting) { 4488 if(getting) { 4489 this.parentWindow.focusedWidget = this; 4490 this.emit!FocusEvent(); 4491 this.emit!FocusInEvent(); 4492 } else { 4493 this.emit!BlurEvent(); 4494 this.emit!FocusOutEvent(); 4495 } 4496 }; 4497 +/ 4498 4499 win.onDestroyed = () { 4500 this.dispose(); 4501 }; 4502 4503 version(win32_widgets) { 4504 Widget.nativeMapping[win.hwnd] = this; 4505 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4506 } else { 4507 win.setEventHandlers( 4508 (MouseEvent e) { 4509 Widget p = this; 4510 while(p ! is parentWindow) { 4511 e.x += p.x; 4512 e.y += p.y; 4513 p = p.parent; 4514 } 4515 parentWindow.dispatchMouseEvent(e); 4516 }, 4517 (KeyEvent e) { 4518 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4519 parentWindow.dispatchKeyEvent(e); 4520 }, 4521 (dchar e) { 4522 parentWindow.dispatchCharEvent(e); 4523 }, 4524 ); 4525 } 4526 4527 } 4528 4529 override void showing(bool s, bool recalc) { 4530 auto cur = hidden; 4531 win.hidden = !s; 4532 if(cur != s && s) 4533 redraw(); 4534 } 4535 4536 /// OpenGL widgets cannot have child widgets. Do not call this. 4537 /* @disable */ final override void addChild(Widget, int) { 4538 throw new Error("cannot add children to OpenGL widgets"); 4539 } 4540 4541 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4542 /// Keep in mind that events like mouse coordinates are still relative to your size. 4543 override void registerMovement() { 4544 // writefln("%d %d %d %d", x,y,width,height); 4545 version(win32_widgets) 4546 auto pos = getChildPositionRelativeToParentHwnd(this); 4547 else 4548 auto pos = getChildPositionRelativeToParentOrigin(this); 4549 win.moveResize(pos[0], pos[1], width, height); 4550 4551 registerMovementAdditionalWork(); 4552 sendResizeEvent(); 4553 } 4554 4555 abstract void registerMovementAdditionalWork(); 4556 } 4557 4558 /++ 4559 Nests an opengl capable window inside this window as a widget. 4560 4561 You may also just want to create an additional [SimpleWindow] with 4562 [OpenGlOptions.yes] yourself. 4563 4564 An OpenGL widget cannot have child widgets. It will throw if you try. 4565 +/ 4566 static if(OpenGlEnabled) 4567 class OpenGlWidget : NestedChildWindowWidget { 4568 4569 override void registerMovementAdditionalWork() { 4570 win.setAsCurrentOpenGlContext(); 4571 } 4572 4573 /// 4574 this(Widget parent) { 4575 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4576 super(win, parent); 4577 } 4578 4579 override void paint(WidgetPainter painter) { 4580 win.setAsCurrentOpenGlContext(); 4581 glViewport(0, 0, this.width, this.height); 4582 win.redrawOpenGlSceneNow(); 4583 } 4584 4585 void redrawOpenGlScene(void delegate() dg) { 4586 win.redrawOpenGlScene = dg; 4587 } 4588 } 4589 4590 /++ 4591 This demo shows how to draw text in an opengl scene. 4592 +/ 4593 unittest { 4594 import arsd.minigui; 4595 import arsd.ttf; 4596 4597 void main() { 4598 auto window = new Window(); 4599 4600 auto widget = new OpenGlWidget(window); 4601 4602 // old means non-shader code so compatible with glBegin etc. 4603 // tbh I haven't implemented new one in font yet... 4604 // anyway, declaring here, will construct soon. 4605 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4606 4607 // this is a little bit awkward, calling some methods through 4608 // the underlying SimpleWindow `win` method, and you can't do this 4609 // on a nanovega widget due to conflicts so I should probably fix 4610 // the api to be a bit easier. But here it will work. 4611 // 4612 // Alternatively, you could load the font on the first draw, inside 4613 // the redrawOpenGlScene, and keep a flag so you don't do it every 4614 // time. That'd be a bit easier since the lib sets up the context 4615 // by then guaranteed. 4616 // 4617 // But still, I wanna show this. 4618 widget.win.visibleForTheFirstTime = delegate { 4619 // must set the opengl context 4620 widget.win.setAsCurrentOpenGlContext(); 4621 4622 // if you were doing a OpenGL 3+ shader, this 4623 // gets especially important to do in order. With 4624 // old-style opengl, I think you can even do it 4625 // in main(), but meh, let's show it more correctly. 4626 4627 // Anyway, now it is time to load the font from the 4628 // OS (you can alternatively load one from a .ttf file 4629 // you bundle with the application), then load the 4630 // font into texture for drawing. 4631 4632 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4633 4634 assert(!osfont.isNull()); // make sure it actually loaded 4635 4636 // using typeof to avoid repeating the long name lol 4637 glfont = new typeof(glfont)( 4638 // get the raw data from the font for loading in here 4639 // since it doesn't use the OS function to draw the 4640 // text, we gotta treat it more as a file than as 4641 // a drawing api. 4642 osfont.getTtfBytes(), 4643 18, // need to respecify size since opengl world is different coordinate system 4644 4645 // these last two numbers are why it is called 4646 // "Limited" font. It only loads the characters 4647 // in the given range, since the texture atlas 4648 // it references is all a big image generated ahead 4649 // of time. You could maybe do the whole thing but 4650 // idk how much memory that is. 4651 // 4652 // But here, 0-128 represents the ASCII range, so 4653 // good enough for most English things, numeric labels, 4654 // etc. 4655 0, 4656 128 4657 ); 4658 }; 4659 4660 widget.redrawOpenGlScene = () { 4661 // now we can use the glfont's drawString function 4662 4663 // first some opengl setup. You can do this in one place 4664 // on window first visible too in many cases, just showing 4665 // here cuz it is easier for me. 4666 4667 // gonna need some alpha blending or it just looks awful 4668 glEnable(GL_BLEND); 4669 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4670 glClearColor(0,0,0,0); 4671 glDepthFunc(GL_LEQUAL); 4672 4673 // Also need to enable 2d textures, since it draws the 4674 // font characters as images baked in 4675 glMatrixMode(GL_MODELVIEW); 4676 glLoadIdentity(); 4677 glDisable(GL_DEPTH_TEST); 4678 glEnable(GL_TEXTURE_2D); 4679 4680 // the orthographic matrix is best for 2d things like text 4681 // so let's set that up. This matrix makes the coordinates 4682 // in the opengl scene be one-to-one with the actual pixels 4683 // on screen. (Not necessarily best, you may wish to scale 4684 // things, but it does help keep fonts looking normal.) 4685 glMatrixMode(GL_PROJECTION); 4686 glLoadIdentity(); 4687 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4688 4689 // you can do other glScale, glRotate, glTranslate, etc 4690 // to the matrix here of course if you want. 4691 4692 // note the x,y coordinates here are for the text baseline 4693 // NOT the upper-left corner. The baseline is like the line 4694 // in the notebook you write on. Most the letters are actually 4695 // above it, but some, like p and q, dip a bit below it. 4696 // 4697 // So if you're used to the upper left coordinate like the 4698 // rest of simpledisplay/minigui usually do, do the 4699 // y + glfont.ascent to bring it down a little. So this 4700 // example puts the string in the upper left of the window. 4701 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4702 4703 // re color btw: the function sets a solid color internally, 4704 // but you actually COULD do your own thing for rainbow effects 4705 // and the sort if you wanted too, by pulling its guts out. 4706 // Just view its source for an idea of how it actually draws: 4707 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4708 4709 // it gets a bit complicated with the character positioning, 4710 // but the opengl parts are fairly simple: bind a texture, 4711 // set the color, draw a quad for each letter. 4712 4713 4714 // the last optional argument there btw is a bounding box 4715 // it will/ use to word wrap and return an object you can 4716 // use to implement scrolling or pagination; it tells how 4717 // much of the string didn't fit in the box. But for simple 4718 // labels we can just ignore that. 4719 4720 4721 // I'd suggest drawing text as the last step, after you 4722 // do your other drawing. You might use the push/pop matrix 4723 // stuff to keep your place. You, in theory, should be able 4724 // to do text in a 3d space but I've never actually tried 4725 // that.... 4726 }; 4727 4728 window.loop(); 4729 } 4730 } 4731 4732 version(custom_widgets) 4733 private alias ListWidgetBase = ScrollableWidget; 4734 else 4735 private alias ListWidgetBase = Widget; 4736 4737 /++ 4738 A list widget contains a list of strings that the user can examine and select. 4739 4740 4741 In the future, items in the list may be possible to be more than just strings. 4742 4743 See_Also: 4744 [TableView] 4745 +/ 4746 class ListWidget : ListWidgetBase { 4747 /// 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. 4748 mixin Emits!(ChangeEvent!void); 4749 4750 static struct Option { 4751 string label; 4752 bool selected; 4753 void* tag; 4754 } 4755 4756 /++ 4757 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4758 +/ 4759 void setSelection(int y) { 4760 if(!multiSelect) 4761 foreach(ref opt; options) 4762 opt.selected = false; 4763 if(y >= 0 && y < options.length) 4764 options[y].selected = !options[y].selected; 4765 4766 this.emit!(ChangeEvent!void)(delegate {}); 4767 4768 version(custom_widgets) 4769 redraw(); 4770 } 4771 4772 /++ 4773 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4774 Returns -1 if nothing is selected. 4775 +/ 4776 int getSelection() 4777 { 4778 foreach(i, opt; options) { 4779 if (opt.selected) 4780 return cast(int) i; 4781 } 4782 return -1; 4783 } 4784 4785 version(custom_widgets) 4786 override void defaultEventHandler_click(ClickEvent event) { 4787 this.focus(); 4788 if(event.button == MouseButton.left) { 4789 auto y = (event.clientY - 4) / defaultLineHeight; 4790 if(y >= 0 && y < options.length) { 4791 setSelection(y); 4792 } 4793 } 4794 super.defaultEventHandler_click(event); 4795 } 4796 4797 this(Widget parent) { 4798 tabStop = false; 4799 super(parent); 4800 version(win32_widgets) 4801 createWin32Window(this, WC_LISTBOX, "", 4802 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4803 } 4804 4805 version(win32_widgets) 4806 override void handleWmCommand(ushort code, ushort id) { 4807 switch(code) { 4808 case LBN_SELCHANGE: 4809 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4810 setSelection(cast(int) sel); 4811 break; 4812 default: 4813 } 4814 } 4815 4816 4817 version(custom_widgets) 4818 override void paintFrameAndBackground(WidgetPainter painter) { 4819 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4820 } 4821 4822 version(custom_widgets) 4823 override void paint(WidgetPainter painter) { 4824 auto cs = getComputedStyle(); 4825 auto pos = Point(4, 4); 4826 foreach(idx, option; options) { 4827 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4828 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4829 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4830 if(option.selected) { 4831 //painter.rasterOp = RasterOp.xor; 4832 painter.outlineColor = cs.selectionForegroundColor; 4833 painter.fillColor = cs.selectionBackgroundColor; 4834 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4835 //painter.rasterOp = RasterOp.normal; 4836 } 4837 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4838 painter.drawText(pos, option.label); 4839 pos.y += defaultLineHeight; 4840 } 4841 } 4842 4843 static class Style : Widget.Style { 4844 override WidgetBackground background() { 4845 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4846 } 4847 } 4848 mixin OverrideStyle!Style; 4849 //mixin Padding!q{2}; 4850 4851 void addOption(string text, void* tag = null) { 4852 options ~= Option(text, false, tag); 4853 version(win32_widgets) { 4854 WCharzBuffer buffer = WCharzBuffer(text); 4855 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4856 } 4857 version(custom_widgets) { 4858 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4859 redraw(); 4860 } 4861 } 4862 4863 void clear() { 4864 options = null; 4865 version(win32_widgets) { 4866 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4867 {} 4868 4869 } else version(custom_widgets) { 4870 scrollTo(Point(0, 0)); 4871 redraw(); 4872 } 4873 } 4874 4875 Option[] options; 4876 version(win32_widgets) 4877 enum multiSelect = false; /// not implemented yet 4878 else 4879 bool multiSelect; 4880 4881 override int heightStretchiness() { return 6; } 4882 } 4883 4884 4885 4886 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4887 enum ScrollBarShowPolicy { 4888 automatic, /// automatically show the scroll bar if it is necessary 4889 never, /// never show the scroll bar (scrolling must be done programmatically) 4890 always /// always show the scroll bar, even if it is disabled 4891 } 4892 4893 /++ 4894 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4895 4896 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4897 +/ 4898 // FIXME ScrollBarShowPolicy 4899 // FIXME: use the ScrollMessageWidget in here now that it exists 4900 class ScrollableWidget : Widget { 4901 // FIXME: make line size configurable 4902 // FIXME: add keyboard controls 4903 version(win32_widgets) { 4904 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4905 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4906 auto pos = HIWORD(wParam); 4907 auto m = LOWORD(wParam); 4908 4909 // FIXME: I can reintroduce the 4910 // scroll bars now by using this 4911 // in the top-level window handler 4912 // to forward comamnds 4913 auto scrollbarHwnd = lParam; 4914 switch(m) { 4915 case SB_BOTTOM: 4916 if(msg == WM_HSCROLL) 4917 horizontalScrollTo(contentWidth_); 4918 else 4919 verticalScrollTo(contentHeight_); 4920 break; 4921 case SB_TOP: 4922 if(msg == WM_HSCROLL) 4923 horizontalScrollTo(0); 4924 else 4925 verticalScrollTo(0); 4926 break; 4927 case SB_ENDSCROLL: 4928 // idk 4929 break; 4930 case SB_LINEDOWN: 4931 if(msg == WM_HSCROLL) 4932 horizontalScroll(scaleWithDpi(16)); 4933 else 4934 verticalScroll(scaleWithDpi(16)); 4935 break; 4936 case SB_LINEUP: 4937 if(msg == WM_HSCROLL) 4938 horizontalScroll(scaleWithDpi(-16)); 4939 else 4940 verticalScroll(scaleWithDpi(-16)); 4941 break; 4942 case SB_PAGEDOWN: 4943 if(msg == WM_HSCROLL) 4944 horizontalScroll(scaleWithDpi(100)); 4945 else 4946 verticalScroll(scaleWithDpi(100)); 4947 break; 4948 case SB_PAGEUP: 4949 if(msg == WM_HSCROLL) 4950 horizontalScroll(scaleWithDpi(-100)); 4951 else 4952 verticalScroll(scaleWithDpi(-100)); 4953 break; 4954 case SB_THUMBPOSITION: 4955 case SB_THUMBTRACK: 4956 if(msg == WM_HSCROLL) 4957 horizontalScrollTo(pos); 4958 else 4959 verticalScrollTo(pos); 4960 4961 if(m == SB_THUMBTRACK) { 4962 // the event loop doesn't seem to carry on with a requested redraw.. 4963 // so we request it to get our dirty bit set... 4964 redraw(); 4965 4966 // then we need to immediately actually redraw it too for instant feedback to user 4967 4968 SimpleWindow.processAllCustomEvents(); 4969 //if(parentWindow) 4970 //parentWindow.actualRedraw(); 4971 } 4972 break; 4973 default: 4974 } 4975 } 4976 return super.hookedWndProc(msg, wParam, lParam); 4977 } 4978 } 4979 /// 4980 this(Widget parent) { 4981 this.parentWindow = parent.parentWindow; 4982 4983 version(win32_widgets) { 4984 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4985 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4986 super(parent); 4987 } else version(custom_widgets) { 4988 outerContainer = new InternalScrollableContainerWidget(this, parent); 4989 super(outerContainer); 4990 } else static assert(0); 4991 } 4992 4993 version(custom_widgets) 4994 InternalScrollableContainerWidget outerContainer; 4995 4996 override void defaultEventHandler_click(ClickEvent event) { 4997 if(event.button == MouseButton.wheelUp) 4998 verticalScroll(scaleWithDpi(-16)); 4999 if(event.button == MouseButton.wheelDown) 5000 verticalScroll(scaleWithDpi(16)); 5001 super.defaultEventHandler_click(event); 5002 } 5003 5004 override void defaultEventHandler_keydown(KeyDownEvent event) { 5005 switch(event.key) { 5006 case Key.Left: 5007 horizontalScroll(scaleWithDpi(-16)); 5008 break; 5009 case Key.Right: 5010 horizontalScroll(scaleWithDpi(16)); 5011 break; 5012 case Key.Up: 5013 verticalScroll(scaleWithDpi(-16)); 5014 break; 5015 case Key.Down: 5016 verticalScroll(scaleWithDpi(16)); 5017 break; 5018 case Key.Home: 5019 verticalScrollTo(0); 5020 break; 5021 case Key.End: 5022 verticalScrollTo(contentHeight); 5023 break; 5024 case Key.PageUp: 5025 verticalScroll(scaleWithDpi(-160)); 5026 break; 5027 case Key.PageDown: 5028 verticalScroll(scaleWithDpi(160)); 5029 break; 5030 default: 5031 } 5032 super.defaultEventHandler_keydown(event); 5033 } 5034 5035 5036 version(win32_widgets) 5037 override void recomputeChildLayout() { 5038 super.recomputeChildLayout(); 5039 SCROLLINFO info; 5040 info.cbSize = info.sizeof; 5041 info.nPage = viewportHeight; 5042 info.fMask = SIF_PAGE | SIF_RANGE; 5043 info.nMin = 0; 5044 info.nMax = contentHeight_; 5045 SetScrollInfo(hwnd, SB_VERT, &info, true); 5046 5047 info.cbSize = info.sizeof; 5048 info.nPage = viewportWidth; 5049 info.fMask = SIF_PAGE | SIF_RANGE; 5050 info.nMin = 0; 5051 info.nMax = contentWidth_; 5052 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5053 } 5054 5055 /* 5056 Scrolling 5057 ------------ 5058 5059 You are assigned a width and a height by the layout engine, which 5060 is your viewport box. However, you may draw more than that by setting 5061 a contentWidth and contentHeight. 5062 5063 If these can be contained by the viewport, no scrollbar is displayed. 5064 If they cannot fit though, it will automatically show scroll as necessary. 5065 5066 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5067 is zero, no vertical scrolling is performed. 5068 5069 If scrolling is necessary, the lib will automatically work with the bars. 5070 When you redraw, the origin and clipping info in the painter is set so if 5071 you just draw everything, it will work, but you can be more efficient by checking 5072 the viewportWidth, viewportHeight, and scrollOrigin members. 5073 */ 5074 5075 /// 5076 final @property int viewportWidth() { 5077 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5078 } 5079 /// 5080 final @property int viewportHeight() { 5081 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5082 } 5083 5084 // FIXME property 5085 Point scrollOrigin_; 5086 5087 /// 5088 final const(Point) scrollOrigin() { 5089 return scrollOrigin_; 5090 } 5091 5092 // the user sets these two 5093 private int contentWidth_ = 0; 5094 private int contentHeight_ = 0; 5095 5096 /// 5097 int contentWidth() { return contentWidth_; } 5098 /// 5099 int contentHeight() { return contentHeight_; } 5100 5101 /// 5102 void setContentSize(int width, int height) { 5103 contentWidth_ = width; 5104 contentHeight_ = height; 5105 5106 version(custom_widgets) { 5107 if(showingVerticalScroll || showingHorizontalScroll) { 5108 outerContainer.queueRecomputeChildLayout(); 5109 } 5110 5111 if(showingVerticalScroll()) 5112 outerContainer.verticalScrollBar.redraw(); 5113 if(showingHorizontalScroll()) 5114 outerContainer.horizontalScrollBar.redraw(); 5115 } else version(win32_widgets) { 5116 queueRecomputeChildLayout(); 5117 } else static assert(0); 5118 } 5119 5120 /// 5121 void verticalScroll(int delta) { 5122 verticalScrollTo(scrollOrigin.y + delta); 5123 } 5124 /// 5125 void verticalScrollTo(int pos) { 5126 scrollOrigin_.y = pos; 5127 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5128 scrollOrigin_.y = contentHeight - viewportHeight; 5129 5130 if(scrollOrigin_.y < 0) 5131 scrollOrigin_.y = 0; 5132 5133 version(win32_widgets) { 5134 SCROLLINFO info; 5135 info.cbSize = info.sizeof; 5136 info.fMask = SIF_POS; 5137 info.nPos = scrollOrigin_.y; 5138 SetScrollInfo(hwnd, SB_VERT, &info, true); 5139 } else version(custom_widgets) { 5140 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5141 } else static assert(0); 5142 5143 redraw(); 5144 } 5145 5146 /// 5147 void horizontalScroll(int delta) { 5148 horizontalScrollTo(scrollOrigin.x + delta); 5149 } 5150 /// 5151 void horizontalScrollTo(int pos) { 5152 scrollOrigin_.x = pos; 5153 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5154 scrollOrigin_.x = contentWidth - viewportWidth; 5155 5156 if(scrollOrigin_.x < 0) 5157 scrollOrigin_.x = 0; 5158 5159 version(win32_widgets) { 5160 SCROLLINFO info; 5161 info.cbSize = info.sizeof; 5162 info.fMask = SIF_POS; 5163 info.nPos = scrollOrigin_.x; 5164 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5165 } else version(custom_widgets) { 5166 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5167 } else static assert(0); 5168 5169 redraw(); 5170 } 5171 /// 5172 void scrollTo(Point p) { 5173 verticalScrollTo(p.y); 5174 horizontalScrollTo(p.x); 5175 } 5176 5177 /// 5178 void ensureVisibleInScroll(Point p) { 5179 auto rect = viewportRectangle(); 5180 if(rect.contains(p)) 5181 return; 5182 if(p.x < rect.left) 5183 horizontalScroll(p.x - rect.left); 5184 else if(p.x > rect.right) 5185 horizontalScroll(p.x - rect.right); 5186 5187 if(p.y < rect.top) 5188 verticalScroll(p.y - rect.top); 5189 else if(p.y > rect.bottom) 5190 verticalScroll(p.y - rect.bottom); 5191 } 5192 5193 /// 5194 void ensureVisibleInScroll(Rectangle rect) { 5195 ensureVisibleInScroll(rect.upperLeft); 5196 ensureVisibleInScroll(rect.lowerRight); 5197 } 5198 5199 /// 5200 Rectangle viewportRectangle() { 5201 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5202 } 5203 5204 /// 5205 bool showingHorizontalScroll() { 5206 return contentWidth > width; 5207 } 5208 /// 5209 bool showingVerticalScroll() { 5210 return contentHeight > height; 5211 } 5212 5213 /// This is called before the ordinary paint delegate, 5214 /// giving you a chance to draw the window frame, etc, 5215 /// before the scroll clip takes effect 5216 void paintFrameAndBackground(WidgetPainter painter) { 5217 version(win32_widgets) { 5218 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5219 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5220 // since the pen is null, to fill the whole space, we need the +1 on both. 5221 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5222 SelectObject(painter.impl.hdc, p); 5223 SelectObject(painter.impl.hdc, b); 5224 } 5225 5226 } 5227 5228 // make space for the scroll bar, and that's it. 5229 final override int paddingRight() { return scaleWithDpi(16); } 5230 final override int paddingBottom() { return scaleWithDpi(16); } 5231 5232 /* 5233 END SCROLLING 5234 */ 5235 5236 override WidgetPainter draw() { 5237 int x = this.x, y = this.y; 5238 auto parent = this.parent; 5239 while(parent) { 5240 x += parent.x; 5241 y += parent.y; 5242 parent = parent.parent; 5243 } 5244 5245 //version(win32_widgets) { 5246 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5247 //} else { 5248 auto painter = parentWindow.win.draw(true); 5249 //} 5250 painter.originX = x; 5251 painter.originY = y; 5252 5253 painter.originX = painter.originX - scrollOrigin.x; 5254 painter.originY = painter.originY - scrollOrigin.y; 5255 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5256 5257 return WidgetPainter(painter, this); 5258 } 5259 5260 mixin ScrollableChildren; 5261 } 5262 5263 // you need to have a Point scrollOrigin in the class somewhere 5264 // and a paintFrameAndBackground 5265 private mixin template ScrollableChildren() { 5266 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5267 if(hidden) 5268 return; 5269 5270 //version(win32_widgets) 5271 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5272 5273 painter.originX = lox + x; 5274 painter.originY = loy + y; 5275 5276 bool actuallyPainted = false; 5277 5278 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5279 if(clip == Rectangle.init) 5280 return; 5281 5282 if(force || redrawRequested) { 5283 //painter.setClipRectangle(scrollOrigin, width, height); 5284 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5285 paintFrameAndBackground(painter); 5286 } 5287 5288 /+ 5289 version(win32_widgets) { 5290 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 5291 } 5292 +/ 5293 5294 painter.originX = painter.originX - scrollOrigin.x; 5295 painter.originY = painter.originY - scrollOrigin.y; 5296 if(force || redrawRequested) { 5297 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5298 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5299 5300 //erase(painter); // we paintFrameAndBackground above so no need 5301 if(painter.visualTheme) 5302 painter.visualTheme.doPaint(this, painter); 5303 else 5304 paint(painter); 5305 5306 if(invalidate) { 5307 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5308 // children are contained inside this, so no need to do extra work 5309 invalidate = false; 5310 } 5311 5312 5313 actuallyPainted = true; 5314 redrawRequested = false; 5315 } 5316 5317 foreach(child; children) { 5318 if(cast(FixedPosition) child) 5319 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5320 else 5321 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5322 } 5323 } 5324 } 5325 5326 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5327 ScrollableContainerWidget scw; 5328 5329 this(ScrollableContainerWidget parent) { 5330 scw = parent; 5331 super(parent); 5332 } 5333 5334 version(custom_widgets) 5335 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5336 if(hidden) 5337 return; 5338 5339 bool actuallyPainted = false; 5340 5341 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5342 5343 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5344 if(clip == Rectangle.init) 5345 return; 5346 5347 painter.originX = lox + x - scrollOrigin.x; 5348 painter.originY = loy + y - scrollOrigin.y; 5349 if(force || redrawRequested) { 5350 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5351 5352 erase(painter); 5353 if(painter.visualTheme) 5354 painter.visualTheme.doPaint(this, painter); 5355 else 5356 paint(painter); 5357 5358 if(invalidate) { 5359 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5360 // children are contained inside this, so no need to do extra work 5361 invalidate = false; 5362 } 5363 5364 actuallyPainted = true; 5365 redrawRequested = false; 5366 } 5367 foreach(child; children) { 5368 if(cast(FixedPosition) child) 5369 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5370 else 5371 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5372 } 5373 } 5374 5375 version(custom_widgets) 5376 override protected void addScrollPosition(ref int x, ref int y) { 5377 x += scw.scrollX_; 5378 y += scw.scrollY_; 5379 } 5380 } 5381 5382 /++ 5383 A widget meant to contain other widgets that may need to scroll. 5384 5385 Currently buggy. 5386 5387 History: 5388 Added July 1, 2021 (dub v10.2) 5389 5390 On January 3, 2022, I tried to use it in a few other cases 5391 and found it only worked well in the original test case. Since 5392 it still sucks, I think I'm going to rewrite it again. 5393 +/ 5394 class ScrollableContainerWidget : ContainerWidget { 5395 /// 5396 this(Widget parent) { 5397 super(parent); 5398 5399 container = new InternalScrollableContainerInsideWidget(this); 5400 hsb = new HorizontalScrollbar(this); 5401 vsb = new VerticalScrollbar(this); 5402 5403 tabStop = false; 5404 container.tabStop = false; 5405 magic = true; 5406 5407 5408 vsb.addEventListener("scrolltonextline", () { 5409 scrollBy(0, scaleWithDpi(16)); 5410 }); 5411 vsb.addEventListener("scrolltopreviousline", () { 5412 scrollBy(0,scaleWithDpi( -16)); 5413 }); 5414 vsb.addEventListener("scrolltonextpage", () { 5415 scrollBy(0, container.height); 5416 }); 5417 vsb.addEventListener("scrolltopreviouspage", () { 5418 scrollBy(0, -container.height); 5419 }); 5420 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5421 scrollTo(scrollX_, spe.value); 5422 }); 5423 5424 this.addEventListener(delegate (scope ClickEvent e) { 5425 if(e.button == MouseButton.wheelUp) { 5426 if(!e.defaultPrevented) 5427 scrollBy(0, scaleWithDpi(-16)); 5428 e.stopPropagation(); 5429 } else if(e.button == MouseButton.wheelDown) { 5430 if(!e.defaultPrevented) 5431 scrollBy(0, scaleWithDpi(16)); 5432 e.stopPropagation(); 5433 } 5434 }); 5435 } 5436 5437 /+ 5438 override void defaultEventHandler_click(ClickEvent e) { 5439 } 5440 +/ 5441 5442 override void removeAllChildren() { 5443 container.removeAllChildren(); 5444 } 5445 5446 void scrollTo(int x, int y) { 5447 scrollBy(x - scrollX_, y - scrollY_); 5448 } 5449 5450 void scrollBy(int x, int y) { 5451 auto ox = scrollX_; 5452 auto oy = scrollY_; 5453 5454 auto nx = ox + x; 5455 auto ny = oy + y; 5456 5457 if(nx < 0) 5458 nx = 0; 5459 if(ny < 0) 5460 ny = 0; 5461 5462 auto maxX = hsb.max - container.width; 5463 if(maxX < 0) maxX = 0; 5464 auto maxY = vsb.max - container.height; 5465 if(maxY < 0) maxY = 0; 5466 5467 if(nx > maxX) 5468 nx = maxX; 5469 if(ny > maxY) 5470 ny = maxY; 5471 5472 auto dx = nx - ox; 5473 auto dy = ny - oy; 5474 5475 if(dx || dy) { 5476 version(win32_widgets) 5477 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5478 else { 5479 redraw(); 5480 } 5481 5482 hsb.setPosition = nx; 5483 vsb.setPosition = ny; 5484 5485 scrollX_ = nx; 5486 scrollY_ = ny; 5487 } 5488 } 5489 5490 private int scrollX_; 5491 private int scrollY_; 5492 5493 void setTotalArea(int width, int height) { 5494 hsb.setMax(width); 5495 vsb.setMax(height); 5496 } 5497 5498 /// 5499 void setViewableArea(int width, int height) { 5500 hsb.setViewableArea(width); 5501 vsb.setViewableArea(height); 5502 } 5503 5504 private bool magic; 5505 override void addChild(Widget w, int position = int.max) { 5506 if(magic) 5507 container.addChild(w, position); 5508 else 5509 super.addChild(w, position); 5510 } 5511 5512 override void recomputeChildLayout() { 5513 if(hsb is null || vsb is null || container is null) return; 5514 5515 /+ 5516 writeln(x, " ", y , " ", width, " ", height); 5517 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5518 +/ 5519 5520 registerMovement(); 5521 5522 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5523 hsb.x = 0; 5524 hsb.y = this.height - hsb.height; 5525 hsb.width = this.width - scaleWithDpi(16); 5526 hsb.recomputeChildLayout(); 5527 5528 vsb.width = scaleWithDpi(16); // FIXME? 5529 vsb.x = this.width - vsb.width; 5530 vsb.y = 0; 5531 vsb.height = this.height - scaleWithDpi(16); 5532 vsb.recomputeChildLayout(); 5533 5534 container.x = 0; 5535 container.y = 0; 5536 container.width = this.width - vsb.width; 5537 container.height = this.height - hsb.height; 5538 container.recomputeChildLayout(); 5539 5540 scrollX_ = 0; 5541 scrollY_ = 0; 5542 5543 hsb.setPosition(0); 5544 vsb.setPosition(0); 5545 5546 int mw, mh; 5547 Widget c = container; 5548 // FIXME: hack here to handle a layout inside... 5549 if(c.children.length == 1 && cast(Layout) c.children[0]) 5550 c = c.children[0]; 5551 foreach(child; c.children) { 5552 auto w = child.x + child.width; 5553 auto h = child.y + child.height; 5554 5555 if(w > mw) mw = w; 5556 if(h > mh) mh = h; 5557 } 5558 5559 setTotalArea(mw, mh); 5560 setViewableArea(width, height); 5561 } 5562 5563 override int minHeight() { return scaleWithDpi(64); } 5564 5565 HorizontalScrollbar hsb; 5566 VerticalScrollbar vsb; 5567 ContainerWidget container; 5568 } 5569 5570 5571 version(custom_widgets) 5572 private class InternalScrollableContainerWidget : Widget { 5573 5574 ScrollableWidget sw; 5575 5576 VerticalScrollbar verticalScrollBar; 5577 HorizontalScrollbar horizontalScrollBar; 5578 5579 this(ScrollableWidget sw, Widget parent) { 5580 this.sw = sw; 5581 5582 this.tabStop = false; 5583 5584 super(parent); 5585 5586 horizontalScrollBar = new HorizontalScrollbar(this); 5587 verticalScrollBar = new VerticalScrollbar(this); 5588 5589 horizontalScrollBar.showing_ = false; 5590 verticalScrollBar.showing_ = false; 5591 5592 horizontalScrollBar.addEventListener("scrolltonextline", { 5593 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5594 sw.horizontalScrollTo(horizontalScrollBar.position); 5595 }); 5596 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5597 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5598 sw.horizontalScrollTo(horizontalScrollBar.position); 5599 }); 5600 verticalScrollBar.addEventListener("scrolltonextline", { 5601 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5602 sw.verticalScrollTo(verticalScrollBar.position); 5603 }); 5604 verticalScrollBar.addEventListener("scrolltopreviousline", { 5605 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5606 sw.verticalScrollTo(verticalScrollBar.position); 5607 }); 5608 horizontalScrollBar.addEventListener("scrolltonextpage", { 5609 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5610 sw.horizontalScrollTo(horizontalScrollBar.position); 5611 }); 5612 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5613 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5614 sw.horizontalScrollTo(horizontalScrollBar.position); 5615 }); 5616 verticalScrollBar.addEventListener("scrolltonextpage", { 5617 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5618 sw.verticalScrollTo(verticalScrollBar.position); 5619 }); 5620 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5621 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5622 sw.verticalScrollTo(verticalScrollBar.position); 5623 }); 5624 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5625 horizontalScrollBar.setPosition(event.intValue); 5626 sw.horizontalScrollTo(horizontalScrollBar.position); 5627 }); 5628 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5629 verticalScrollBar.setPosition(event.intValue); 5630 sw.verticalScrollTo(verticalScrollBar.position); 5631 }); 5632 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5633 horizontalScrollBar.setPosition(event.intValue); 5634 sw.horizontalScrollTo(horizontalScrollBar.position); 5635 }); 5636 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5637 verticalScrollBar.setPosition(event.intValue); 5638 }); 5639 } 5640 5641 // this is supposed to be basically invisible... 5642 override int minWidth() { return sw.minWidth; } 5643 override int minHeight() { return sw.minHeight; } 5644 override int maxWidth() { return sw.maxWidth; } 5645 override int maxHeight() { return sw.maxHeight; } 5646 override int widthStretchiness() { return sw.widthStretchiness; } 5647 override int heightStretchiness() { return sw.heightStretchiness; } 5648 override int marginLeft() { return sw.marginLeft; } 5649 override int marginRight() { return sw.marginRight; } 5650 override int marginTop() { return sw.marginTop; } 5651 override int marginBottom() { return sw.marginBottom; } 5652 override int paddingLeft() { return sw.paddingLeft; } 5653 override int paddingRight() { return sw.paddingRight; } 5654 override int paddingTop() { return sw.paddingTop; } 5655 override int paddingBottom() { return sw.paddingBottom; } 5656 override void focus() { sw.focus(); } 5657 5658 5659 override void recomputeChildLayout() { 5660 // The stupid thing needs to calculate if a scroll bar is needed... 5661 recomputeChildLayoutHelper(); 5662 // then running it again will position things correctly if the bar is NOT needed 5663 recomputeChildLayoutHelper(); 5664 5665 // this sucks but meh it barely works 5666 } 5667 5668 private void recomputeChildLayoutHelper() { 5669 if(sw is null) return; 5670 5671 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5672 if(horizontalScrollBar && verticalScrollBar) { 5673 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5674 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5675 horizontalScrollBar.x = 0; 5676 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5677 5678 verticalScrollBar.width = verticalScrollBar.minWidth(); 5679 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5680 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5681 verticalScrollBar.y = 0 + 2; 5682 5683 sw.x = 0; 5684 sw.y = 0; 5685 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5686 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5687 5688 if(sw.contentWidth_ <= this.width) 5689 sw.scrollOrigin_.x = 0; 5690 if(sw.contentHeight_ <= this.height) 5691 sw.scrollOrigin_.y = 0; 5692 5693 horizontalScrollBar.recomputeChildLayout(); 5694 verticalScrollBar.recomputeChildLayout(); 5695 sw.recomputeChildLayout(); 5696 } 5697 5698 if(sw.contentWidth_ <= this.width) 5699 sw.scrollOrigin_.x = 0; 5700 if(sw.contentHeight_ <= this.height) 5701 sw.scrollOrigin_.y = 0; 5702 5703 if(sw.showingHorizontalScroll()) 5704 horizontalScrollBar.showing(true, false); 5705 else 5706 horizontalScrollBar.showing(false, false); 5707 if(sw.showingVerticalScroll()) 5708 verticalScrollBar.showing(true, false); 5709 else 5710 verticalScrollBar.showing(false, false); 5711 5712 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5713 verticalScrollBar.setMax(sw.contentHeight); 5714 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5715 5716 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5717 horizontalScrollBar.setMax(sw.contentWidth); 5718 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5719 } 5720 } 5721 5722 /* 5723 class ScrollableClientWidget : Widget { 5724 this(Widget parent) { 5725 super(parent); 5726 } 5727 override void paint(WidgetPainter p) { 5728 parent.paint(p); 5729 } 5730 } 5731 */ 5732 5733 /++ 5734 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. 5735 +/ 5736 abstract class Slider : Widget { 5737 this(int min, int max, int step, Widget parent) { 5738 min_ = min; 5739 max_ = max; 5740 step_ = step; 5741 page_ = step; 5742 super(parent); 5743 } 5744 5745 private int min_; 5746 private int max_; 5747 private int step_; 5748 private int position_; 5749 private int page_; 5750 5751 // selection start and selection end 5752 // tics 5753 // tooltip? 5754 // some way to see and just type the value 5755 // win32 buddy controls are labels 5756 5757 /// 5758 void setMin(int a) { 5759 min_ = a; 5760 version(custom_widgets) 5761 redraw(); 5762 version(win32_widgets) 5763 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5764 } 5765 /// 5766 int min() { 5767 return min_; 5768 } 5769 /// 5770 void setMax(int a) { 5771 max_ = a; 5772 version(custom_widgets) 5773 redraw(); 5774 version(win32_widgets) 5775 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5776 } 5777 /// 5778 int max() { 5779 return max_; 5780 } 5781 /// 5782 void setPosition(int a) { 5783 if(a > max) 5784 a = max; 5785 if(a < min) 5786 a = min; 5787 position_ = a; 5788 version(custom_widgets) 5789 setPositionCustom(a); 5790 5791 version(win32_widgets) 5792 setPositionWindows(a); 5793 } 5794 version(win32_widgets) { 5795 protected abstract void setPositionWindows(int a); 5796 } 5797 5798 protected abstract int win32direction(); 5799 5800 /++ 5801 Alias for [position] for better compatibility with generic code. 5802 5803 History: 5804 Added October 5, 2021 5805 +/ 5806 @property int value() { 5807 return position; 5808 } 5809 5810 /// 5811 int position() { 5812 return position_; 5813 } 5814 /// 5815 void setStep(int a) { 5816 step_ = a; 5817 version(win32_widgets) 5818 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5819 } 5820 /// 5821 int step() { 5822 return step_; 5823 } 5824 /// 5825 void setPageSize(int a) { 5826 page_ = a; 5827 version(win32_widgets) 5828 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5829 } 5830 /// 5831 int pageSize() { 5832 return page_; 5833 } 5834 5835 private void notify() { 5836 auto event = new ChangeEvent!int(this, &this.position); 5837 event.dispatch(); 5838 } 5839 5840 version(win32_widgets) 5841 void win32Setup(int style) { 5842 createWin32Window(this, TRACKBAR_CLASS, "", 5843 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5844 5845 // the trackbar sends the same messages as scroll, which 5846 // our other layer sends as these... just gonna translate 5847 // here 5848 this.addDirectEventListener("scrolltoposition", (Event event) { 5849 event.stopPropagation(); 5850 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5851 notify(); 5852 }); 5853 this.addDirectEventListener("scrolltonextline", (Event event) { 5854 event.stopPropagation(); 5855 this.setPosition(this.position + this.step_ * this.win32direction); 5856 notify(); 5857 }); 5858 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5859 event.stopPropagation(); 5860 this.setPosition(this.position - this.step_ * this.win32direction); 5861 notify(); 5862 }); 5863 this.addDirectEventListener("scrolltonextpage", (Event event) { 5864 event.stopPropagation(); 5865 this.setPosition(this.position + this.page_ * this.win32direction); 5866 notify(); 5867 }); 5868 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5869 event.stopPropagation(); 5870 this.setPosition(this.position - this.page_ * this.win32direction); 5871 notify(); 5872 }); 5873 5874 setMin(min_); 5875 setMax(max_); 5876 setStep(step_); 5877 setPageSize(page_); 5878 } 5879 5880 version(custom_widgets) { 5881 protected MouseTrackingWidget thumb; 5882 5883 protected abstract void setPositionCustom(int a); 5884 5885 override void defaultEventHandler_keydown(KeyDownEvent event) { 5886 switch(event.key) { 5887 case Key.Up: 5888 case Key.Right: 5889 setPosition(position() - step() * win32direction); 5890 changed(); 5891 break; 5892 case Key.Down: 5893 case Key.Left: 5894 setPosition(position() + step() * win32direction); 5895 changed(); 5896 break; 5897 case Key.Home: 5898 setPosition(win32direction > 0 ? min() : max()); 5899 changed(); 5900 break; 5901 case Key.End: 5902 setPosition(win32direction > 0 ? max() : min()); 5903 changed(); 5904 break; 5905 case Key.PageUp: 5906 setPosition(position() - pageSize() * win32direction); 5907 changed(); 5908 break; 5909 case Key.PageDown: 5910 setPosition(position() + pageSize() * win32direction); 5911 changed(); 5912 break; 5913 default: 5914 } 5915 super.defaultEventHandler_keydown(event); 5916 } 5917 5918 protected void changed() { 5919 auto ev = new ChangeEvent!int(this, &position); 5920 ev.dispatch(); 5921 } 5922 } 5923 } 5924 5925 /++ 5926 5927 +/ 5928 class VerticalSlider : Slider { 5929 this(int min, int max, int step, Widget parent) { 5930 version(custom_widgets) 5931 initialize(); 5932 5933 super(min, max, step, parent); 5934 5935 version(win32_widgets) 5936 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5937 } 5938 5939 protected override int win32direction() { 5940 return -1; 5941 } 5942 5943 version(win32_widgets) 5944 protected override void setPositionWindows(int a) { 5945 // the windows thing makes the top 0 and i don't like that. 5946 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5947 } 5948 5949 version(custom_widgets) 5950 private void initialize() { 5951 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5952 5953 thumb.tabStop = false; 5954 5955 thumb.thumbWidth = width; 5956 thumb.thumbHeight = scaleWithDpi(16); 5957 5958 thumb.addEventListener(EventType.change, () { 5959 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5960 sx = max - sx; 5961 //informProgramThatUserChangedPosition(sx); 5962 5963 position_ = sx; 5964 5965 changed(); 5966 }); 5967 } 5968 5969 version(custom_widgets) 5970 override void recomputeChildLayout() { 5971 thumb.thumbWidth = this.width; 5972 super.recomputeChildLayout(); 5973 setPositionCustom(position_); 5974 } 5975 5976 version(custom_widgets) 5977 protected override void setPositionCustom(int a) { 5978 if(max()) 5979 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5980 redraw(); 5981 } 5982 } 5983 5984 /++ 5985 5986 +/ 5987 class HorizontalSlider : Slider { 5988 this(int min, int max, int step, Widget parent) { 5989 version(custom_widgets) 5990 initialize(); 5991 5992 super(min, max, step, parent); 5993 5994 version(win32_widgets) 5995 win32Setup(TBS_HORZ); 5996 } 5997 5998 version(win32_widgets) 5999 protected override void setPositionWindows(int a) { 6000 SendMessage(hwnd, TBM_SETPOS, true, a); 6001 } 6002 6003 protected override int win32direction() { 6004 return 1; 6005 } 6006 6007 version(custom_widgets) 6008 private void initialize() { 6009 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 6010 6011 thumb.tabStop = false; 6012 6013 thumb.thumbWidth = scaleWithDpi(16); 6014 thumb.thumbHeight = height; 6015 6016 thumb.addEventListener(EventType.change, () { 6017 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6018 //informProgramThatUserChangedPosition(sx); 6019 6020 position_ = sx; 6021 6022 changed(); 6023 }); 6024 } 6025 6026 version(custom_widgets) 6027 override void recomputeChildLayout() { 6028 thumb.thumbHeight = this.height; 6029 super.recomputeChildLayout(); 6030 setPositionCustom(position_); 6031 } 6032 6033 version(custom_widgets) 6034 protected override void setPositionCustom(int a) { 6035 if(max()) 6036 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6037 redraw(); 6038 } 6039 } 6040 6041 6042 /// 6043 abstract class ScrollbarBase : Widget { 6044 /// 6045 this(Widget parent) { 6046 super(parent); 6047 tabStop = false; 6048 step_ = scaleWithDpi(16); 6049 } 6050 6051 private int viewableArea_; 6052 private int max_; 6053 private int step_;// = 16; 6054 private int position_; 6055 6056 /// 6057 bool atEnd() { 6058 return position_ + viewableArea_ >= max_; 6059 } 6060 6061 /// 6062 bool atStart() { 6063 return position_ == 0; 6064 } 6065 6066 /// 6067 void setViewableArea(int a) { 6068 viewableArea_ = a; 6069 version(custom_widgets) 6070 redraw(); 6071 } 6072 /// 6073 void setMax(int a) { 6074 max_ = a; 6075 version(custom_widgets) 6076 redraw(); 6077 } 6078 /// 6079 int max() { 6080 return max_; 6081 } 6082 /// 6083 void setPosition(int a) { 6084 auto logicalMax = max_ - viewableArea_; 6085 if(a == int.max) 6086 a = logicalMax; 6087 6088 if(a > logicalMax) 6089 a = logicalMax; 6090 if(a < 0) 6091 a = 0; 6092 6093 position_ = a; 6094 6095 version(custom_widgets) 6096 redraw(); 6097 } 6098 /// 6099 int position() { 6100 return position_; 6101 } 6102 /// 6103 void setStep(int a) { 6104 step_ = a; 6105 } 6106 /// 6107 int step() { 6108 return step_; 6109 } 6110 6111 // FIXME: remove this.... maybe 6112 /+ 6113 protected void informProgramThatUserChangedPosition(int n) { 6114 position_ = n; 6115 auto evt = new Event(EventType.change, this); 6116 evt.intValue = n; 6117 evt.dispatch(); 6118 } 6119 +/ 6120 6121 version(custom_widgets) { 6122 enum MIN_THUMB_SIZE = 8; 6123 6124 abstract protected int getBarDim(); 6125 int thumbSize() { 6126 if(viewableArea_ >= max_ || max_ == 0) 6127 return getBarDim(); 6128 6129 int res = viewableArea_ * getBarDim() / max_; 6130 6131 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6132 res = scaleWithDpi(MIN_THUMB_SIZE); 6133 6134 return res; 6135 } 6136 6137 int thumbPosition() { 6138 /* 6139 viewableArea_ is the viewport height/width 6140 position_ is where we are 6141 */ 6142 //if(position_ + viewableArea_ >= max_) 6143 //return getBarDim - thumbSize; 6144 6145 auto maximumPossibleValue = getBarDim() - thumbSize; 6146 auto maximiumLogicalValue = max_ - viewableArea_; 6147 6148 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6149 6150 return p; 6151 } 6152 } 6153 } 6154 6155 //public import mgt; 6156 6157 /++ 6158 A mouse tracking widget is one that follows the mouse when dragged inside it. 6159 6160 Concrete subclasses may include a scrollbar thumb and a volume control. 6161 +/ 6162 //version(custom_widgets) 6163 class MouseTrackingWidget : Widget { 6164 6165 /// 6166 int positionX() { return positionX_; } 6167 /// 6168 int positionY() { return positionY_; } 6169 6170 /// 6171 void positionX(int p) { positionX_ = p; } 6172 /// 6173 void positionY(int p) { positionY_ = p; } 6174 6175 private int positionX_; 6176 private int positionY_; 6177 6178 /// 6179 enum Orientation { 6180 horizontal, /// 6181 vertical, /// 6182 twoDimensional, /// 6183 } 6184 6185 private int thumbWidth_; 6186 private int thumbHeight_; 6187 6188 /// 6189 int thumbWidth() { return thumbWidth_; } 6190 /// 6191 int thumbHeight() { return thumbHeight_; } 6192 /// 6193 int thumbWidth(int a) { return thumbWidth_ = a; } 6194 /// 6195 int thumbHeight(int a) { return thumbHeight_ = a; } 6196 6197 private bool dragging; 6198 private bool hovering; 6199 private int startMouseX, startMouseY; 6200 6201 /// 6202 this(Orientation orientation, Widget parent) { 6203 super(parent); 6204 6205 //assert(parentWindow !is null); 6206 6207 addEventListener((MouseDownEvent event) { 6208 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6209 dragging = true; 6210 startMouseX = event.clientX - positionX; 6211 startMouseY = event.clientY - positionY; 6212 parentWindow.captureMouse(this); 6213 } else { 6214 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6215 positionX = event.clientX - thumbWidth / 2; 6216 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6217 positionY = event.clientY - thumbHeight / 2; 6218 6219 if(positionX + thumbWidth > this.width) 6220 positionX = this.width - thumbWidth; 6221 if(positionY + thumbHeight > this.height) 6222 positionY = this.height - thumbHeight; 6223 6224 if(positionX < 0) 6225 positionX = 0; 6226 if(positionY < 0) 6227 positionY = 0; 6228 6229 6230 // this.emit!(ChangeEvent!void)(); 6231 auto evt = new Event(EventType.change, this); 6232 evt.sendDirectly(); 6233 6234 redraw(); 6235 6236 } 6237 }); 6238 6239 addEventListener(EventType.mouseup, (Event event) { 6240 dragging = false; 6241 parentWindow.releaseMouseCapture(); 6242 }); 6243 6244 addEventListener(EventType.mouseout, (Event event) { 6245 if(!hovering) 6246 return; 6247 hovering = false; 6248 redraw(); 6249 }); 6250 6251 int lpx, lpy; 6252 6253 addEventListener((MouseMoveEvent event) { 6254 auto oh = hovering; 6255 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6256 hovering = true; 6257 } else { 6258 hovering = false; 6259 } 6260 if(!dragging) { 6261 if(hovering != oh) 6262 redraw(); 6263 return; 6264 } 6265 6266 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6267 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6268 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6269 positionY = event.clientY - startMouseY; 6270 6271 if(positionX + thumbWidth > this.width) 6272 positionX = this.width - thumbWidth; 6273 if(positionY + thumbHeight > this.height) 6274 positionY = this.height - thumbHeight; 6275 6276 if(positionX < 0) 6277 positionX = 0; 6278 if(positionY < 0) 6279 positionY = 0; 6280 6281 if(positionX != lpx || positionY != lpy) { 6282 lpx = positionX; 6283 lpy = positionY; 6284 6285 auto evt = new Event(EventType.change, this); 6286 evt.sendDirectly(); 6287 } 6288 6289 redraw(); 6290 }); 6291 } 6292 6293 version(custom_widgets) 6294 override void paint(WidgetPainter painter) { 6295 auto cs = getComputedStyle(); 6296 auto c = darken(cs.windowBackgroundColor, 0.2); 6297 painter.outlineColor = c; 6298 painter.fillColor = c; 6299 painter.drawRectangle(Point(0, 0), this.width, this.height); 6300 6301 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6302 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6303 } 6304 } 6305 6306 //version(custom_widgets) 6307 //private 6308 class HorizontalScrollbar : ScrollbarBase { 6309 6310 version(custom_widgets) { 6311 private MouseTrackingWidget thumb; 6312 6313 override int getBarDim() { 6314 return thumb.width; 6315 } 6316 } 6317 6318 override void setViewableArea(int a) { 6319 super.setViewableArea(a); 6320 6321 version(win32_widgets) { 6322 SCROLLINFO info; 6323 info.cbSize = info.sizeof; 6324 info.nPage = a + 1; 6325 info.fMask = SIF_PAGE; 6326 SetScrollInfo(hwnd, SB_CTL, &info, true); 6327 } else version(custom_widgets) { 6328 thumb.positionX = thumbPosition; 6329 thumb.thumbWidth = thumbSize; 6330 thumb.redraw(); 6331 } else static assert(0); 6332 6333 } 6334 6335 override void setMax(int a) { 6336 super.setMax(a); 6337 version(win32_widgets) { 6338 SCROLLINFO info; 6339 info.cbSize = info.sizeof; 6340 info.nMin = 0; 6341 info.nMax = max; 6342 info.fMask = SIF_RANGE; 6343 SetScrollInfo(hwnd, SB_CTL, &info, true); 6344 } else version(custom_widgets) { 6345 thumb.positionX = thumbPosition; 6346 thumb.thumbWidth = thumbSize; 6347 thumb.redraw(); 6348 } 6349 } 6350 6351 override void setPosition(int a) { 6352 super.setPosition(a); 6353 version(win32_widgets) { 6354 SCROLLINFO info; 6355 info.cbSize = info.sizeof; 6356 info.fMask = SIF_POS; 6357 info.nPos = position; 6358 SetScrollInfo(hwnd, SB_CTL, &info, true); 6359 } else version(custom_widgets) { 6360 thumb.positionX = thumbPosition(); 6361 thumb.thumbWidth = thumbSize; 6362 thumb.redraw(); 6363 } else static assert(0); 6364 } 6365 6366 this(Widget parent) { 6367 super(parent); 6368 6369 version(win32_widgets) { 6370 createWin32Window(this, "Scrollbar"w, "", 6371 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6372 } else version(custom_widgets) { 6373 auto vl = new HorizontalLayout(this); 6374 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6375 leftButton.setClickRepeat(scrollClickRepeatInterval); 6376 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6377 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6378 rightButton.setClickRepeat(scrollClickRepeatInterval); 6379 6380 leftButton.tabStop = false; 6381 rightButton.tabStop = false; 6382 thumb.tabStop = false; 6383 6384 leftButton.addEventListener(EventType.triggered, () { 6385 this.emitCommand!"scrolltopreviousline"(); 6386 //informProgramThatUserChangedPosition(position - step()); 6387 }); 6388 rightButton.addEventListener(EventType.triggered, () { 6389 this.emitCommand!"scrolltonextline"(); 6390 //informProgramThatUserChangedPosition(position + step()); 6391 }); 6392 6393 thumb.thumbWidth = this.minWidth; 6394 thumb.thumbHeight = scaleWithDpi(16); 6395 6396 thumb.addEventListener(EventType.change, () { 6397 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6398 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6399 6400 //informProgramThatUserChangedPosition(sx); 6401 6402 auto ev = new ScrollToPositionEvent(this, sx); 6403 ev.dispatch(); 6404 }); 6405 } 6406 } 6407 6408 override int minHeight() { return scaleWithDpi(16); } 6409 override int maxHeight() { return scaleWithDpi(16); } 6410 override int minWidth() { return scaleWithDpi(48); } 6411 } 6412 6413 class ScrollToPositionEvent : Event { 6414 enum EventString = "scrolltoposition"; 6415 6416 this(Widget target, int value) { 6417 this.value = value; 6418 super(EventString, target); 6419 } 6420 6421 immutable int value; 6422 6423 override @property int intValue() { 6424 return value; 6425 } 6426 } 6427 6428 //version(custom_widgets) 6429 //private 6430 class VerticalScrollbar : ScrollbarBase { 6431 6432 version(custom_widgets) { 6433 override int getBarDim() { 6434 return thumb.height; 6435 } 6436 6437 private MouseTrackingWidget thumb; 6438 } 6439 6440 override void setViewableArea(int a) { 6441 super.setViewableArea(a); 6442 6443 version(win32_widgets) { 6444 SCROLLINFO info; 6445 info.cbSize = info.sizeof; 6446 info.nPage = a + 1; 6447 info.fMask = SIF_PAGE; 6448 SetScrollInfo(hwnd, SB_CTL, &info, true); 6449 } else version(custom_widgets) { 6450 thumb.positionY = thumbPosition; 6451 thumb.thumbHeight = thumbSize; 6452 thumb.redraw(); 6453 } else static assert(0); 6454 6455 } 6456 6457 override void setMax(int a) { 6458 super.setMax(a); 6459 version(win32_widgets) { 6460 SCROLLINFO info; 6461 info.cbSize = info.sizeof; 6462 info.nMin = 0; 6463 info.nMax = max; 6464 info.fMask = SIF_RANGE; 6465 SetScrollInfo(hwnd, SB_CTL, &info, true); 6466 } else version(custom_widgets) { 6467 thumb.positionY = thumbPosition; 6468 thumb.thumbHeight = thumbSize; 6469 thumb.redraw(); 6470 } 6471 } 6472 6473 override void setPosition(int a) { 6474 super.setPosition(a); 6475 version(win32_widgets) { 6476 SCROLLINFO info; 6477 info.cbSize = info.sizeof; 6478 info.fMask = SIF_POS; 6479 info.nPos = position; 6480 SetScrollInfo(hwnd, SB_CTL, &info, true); 6481 } else version(custom_widgets) { 6482 thumb.positionY = thumbPosition; 6483 thumb.thumbHeight = thumbSize; 6484 thumb.redraw(); 6485 } else static assert(0); 6486 } 6487 6488 this(Widget parent) { 6489 super(parent); 6490 6491 version(win32_widgets) { 6492 createWin32Window(this, "Scrollbar"w, "", 6493 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6494 } else version(custom_widgets) { 6495 auto vl = new VerticalLayout(this); 6496 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6497 upButton.setClickRepeat(scrollClickRepeatInterval); 6498 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6499 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6500 downButton.setClickRepeat(scrollClickRepeatInterval); 6501 6502 upButton.addEventListener(EventType.triggered, () { 6503 this.emitCommand!"scrolltopreviousline"(); 6504 //informProgramThatUserChangedPosition(position - step()); 6505 }); 6506 downButton.addEventListener(EventType.triggered, () { 6507 this.emitCommand!"scrolltonextline"(); 6508 //informProgramThatUserChangedPosition(position + step()); 6509 }); 6510 6511 thumb.thumbWidth = this.minWidth; 6512 thumb.thumbHeight = scaleWithDpi(16); 6513 6514 thumb.addEventListener(EventType.change, () { 6515 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6516 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6517 6518 auto ev = new ScrollToPositionEvent(this, sy); 6519 ev.dispatch(); 6520 6521 //informProgramThatUserChangedPosition(sy); 6522 }); 6523 6524 upButton.tabStop = false; 6525 downButton.tabStop = false; 6526 thumb.tabStop = false; 6527 } 6528 } 6529 6530 override int minWidth() { return scaleWithDpi(16); } 6531 override int maxWidth() { return scaleWithDpi(16); } 6532 override int minHeight() { return scaleWithDpi(48); } 6533 } 6534 6535 6536 /++ 6537 EXPERIMENTAL 6538 6539 A widget specialized for being a container for other widgets. 6540 6541 History: 6542 Added May 29, 2021. Not stabilized at this time. 6543 +/ 6544 class WidgetContainer : Widget { 6545 this(Widget parent) { 6546 tabStop = false; 6547 super(parent); 6548 } 6549 6550 override int maxHeight() { 6551 if(this.children.length == 1) { 6552 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6553 } else { 6554 return int.max; 6555 } 6556 } 6557 6558 override int maxWidth() { 6559 if(this.children.length == 1) { 6560 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6561 } else { 6562 return int.max; 6563 } 6564 } 6565 6566 /+ 6567 6568 override int minHeight() { 6569 int largest = 0; 6570 int margins = 0; 6571 int lastMargin = 0; 6572 foreach(child; children) { 6573 auto mh = child.minHeight(); 6574 if(mh > largest) 6575 largest = mh; 6576 margins += mymax(lastMargin, child.marginTop()); 6577 lastMargin = child.marginBottom(); 6578 } 6579 return largest + margins; 6580 } 6581 6582 override int maxHeight() { 6583 int largest = 0; 6584 int margins = 0; 6585 int lastMargin = 0; 6586 foreach(child; children) { 6587 auto mh = child.maxHeight(); 6588 if(mh == int.max) 6589 return int.max; 6590 if(mh > largest) 6591 largest = mh; 6592 margins += mymax(lastMargin, child.marginTop()); 6593 lastMargin = child.marginBottom(); 6594 } 6595 return largest + margins; 6596 } 6597 6598 override int minWidth() { 6599 int min; 6600 foreach(child; children) { 6601 auto cm = child.minWidth; 6602 if(cm > min) 6603 min = cm; 6604 } 6605 return min + paddingLeft + paddingRight; 6606 } 6607 6608 override int minHeight() { 6609 int min; 6610 foreach(child; children) { 6611 auto cm = child.minHeight; 6612 if(cm > min) 6613 min = cm; 6614 } 6615 return min + paddingTop + paddingBottom; 6616 } 6617 6618 override int maxHeight() { 6619 int largest = 0; 6620 int margins = 0; 6621 int lastMargin = 0; 6622 foreach(child; children) { 6623 auto mh = child.maxHeight(); 6624 if(mh == int.max) 6625 return int.max; 6626 if(mh > largest) 6627 largest = mh; 6628 margins += mymax(lastMargin, child.marginTop()); 6629 lastMargin = child.marginBottom(); 6630 } 6631 return largest + margins; 6632 } 6633 6634 override int heightStretchiness() { 6635 int max; 6636 foreach(child; children) { 6637 auto c = child.heightStretchiness; 6638 if(c > max) 6639 max = c; 6640 } 6641 return max; 6642 } 6643 6644 override int marginTop() { 6645 if(this.children.length) 6646 return this.children[0].marginTop; 6647 return 0; 6648 } 6649 +/ 6650 } 6651 6652 /// 6653 abstract class Layout : Widget { 6654 this(Widget parent) { 6655 tabStop = false; 6656 super(parent); 6657 } 6658 } 6659 6660 /++ 6661 Makes all children minimum width and height, placing them down 6662 left to right, top to bottom. 6663 6664 Useful if you want to make a list of buttons that automatically 6665 wrap to a new line when necessary. 6666 +/ 6667 class InlineBlockLayout : Layout { 6668 /// 6669 this(Widget parent) { super(parent); } 6670 6671 override void recomputeChildLayout() { 6672 registerMovement(); 6673 6674 int x = this.paddingLeft, y = this.paddingTop; 6675 6676 int lineHeight; 6677 int previousMargin = 0; 6678 int previousMarginBottom = 0; 6679 6680 foreach(child; children) { 6681 if(child.hidden) 6682 continue; 6683 if(cast(FixedPosition) child) { 6684 child.recomputeChildLayout(); 6685 continue; 6686 } 6687 child.width = child.flexBasisWidth(); 6688 if(child.width == 0) 6689 child.width = child.minWidth(); 6690 if(child.width == 0) 6691 child.width = 32; 6692 6693 child.height = child.flexBasisHeight(); 6694 if(child.height == 0) 6695 child.height = child.minHeight(); 6696 if(child.height == 0) 6697 child.height = 32; 6698 6699 if(x + child.width + paddingRight > this.width) { 6700 x = this.paddingLeft; 6701 y += lineHeight; 6702 lineHeight = 0; 6703 previousMargin = 0; 6704 previousMarginBottom = 0; 6705 } 6706 6707 auto margin = child.marginLeft; 6708 if(previousMargin > margin) 6709 margin = previousMargin; 6710 6711 x += margin; 6712 6713 child.x = x; 6714 child.y = y; 6715 6716 int marginTopApplied; 6717 if(child.marginTop > previousMarginBottom) { 6718 child.y += child.marginTop; 6719 marginTopApplied = child.marginTop; 6720 } 6721 6722 x += child.width; 6723 previousMargin = child.marginRight; 6724 6725 if(child.marginBottom > previousMarginBottom) 6726 previousMarginBottom = child.marginBottom; 6727 6728 auto h = child.height + previousMarginBottom + marginTopApplied; 6729 if(h > lineHeight) 6730 lineHeight = h; 6731 6732 child.recomputeChildLayout(); 6733 } 6734 6735 } 6736 6737 override int minWidth() { 6738 int min; 6739 foreach(child; children) { 6740 auto cm = child.minWidth; 6741 if(cm > min) 6742 min = cm; 6743 } 6744 return min + paddingLeft + paddingRight; 6745 } 6746 6747 override int minHeight() { 6748 int min; 6749 foreach(child; children) { 6750 auto cm = child.minHeight; 6751 if(cm > min) 6752 min = cm; 6753 } 6754 return min + paddingTop + paddingBottom; 6755 } 6756 } 6757 6758 /++ 6759 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6760 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6761 the [TabWidget] will automatically change pages of child widgets. 6762 6763 This allows you to react to it however you see fit rather than having to 6764 be tied to just the new sets of child widgets. 6765 6766 It sends the message in the form of `this.emitCommand!"changetab"();`. 6767 6768 History: 6769 Added December 24, 2021 (dub v10.5) 6770 +/ 6771 class TabMessageWidget : Widget { 6772 6773 protected void tabIndexClicked(int item) { 6774 this.emitCommand!"changetab"(); 6775 } 6776 6777 /++ 6778 Adds the a new tab to the control with the given title. 6779 6780 Returns: 6781 The index of the newly added tab. You will need to know 6782 this index to refer to it later and to know which tab to 6783 change to when you get a changetab message. 6784 +/ 6785 int addTab(string title, int pos = int.max) { 6786 version(win32_widgets) { 6787 TCITEM item; 6788 item.mask = TCIF_TEXT; 6789 WCharzBuffer buf = WCharzBuffer(title); 6790 item.pszText = buf.ptr; 6791 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6792 } else version(custom_widgets) { 6793 if(pos >= tabs.length) { 6794 tabs ~= title; 6795 redraw(); 6796 return cast(int) tabs.length - 1; 6797 } else if(pos <= 0) { 6798 tabs = title ~ tabs; 6799 redraw(); 6800 return 0; 6801 } else { 6802 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6803 redraw(); 6804 return pos; 6805 } 6806 } 6807 } 6808 6809 override void addChild(Widget child, int pos = int.max) { 6810 if(container) 6811 container.addChild(child, pos); 6812 else 6813 super.addChild(child, pos); 6814 } 6815 6816 protected Widget makeContainer() { 6817 return new Widget(this); 6818 } 6819 6820 private Widget container; 6821 6822 override void recomputeChildLayout() { 6823 version(win32_widgets) { 6824 this.registerMovement(); 6825 6826 RECT rect; 6827 GetWindowRect(hwnd, &rect); 6828 6829 auto left = rect.left; 6830 auto top = rect.top; 6831 6832 TabCtrl_AdjustRect(hwnd, false, &rect); 6833 foreach(child; children) { 6834 if(!child.showing) continue; 6835 child.x = rect.left - left; 6836 child.y = rect.top - top; 6837 child.width = rect.right - rect.left; 6838 child.height = rect.bottom - rect.top; 6839 child.recomputeChildLayout(); 6840 } 6841 } else version(custom_widgets) { 6842 this.registerMovement(); 6843 foreach(child; children) { 6844 if(!child.showing) continue; 6845 child.x = 2; 6846 child.y = tabBarHeight + 2; // for the border 6847 child.width = width - 4; // for the border 6848 child.height = height - tabBarHeight - 2 - 2; // for the border 6849 child.recomputeChildLayout(); 6850 } 6851 } else static assert(0); 6852 } 6853 6854 version(custom_widgets) 6855 string[] tabs; 6856 6857 this(Widget parent) { 6858 super(parent); 6859 6860 tabStop = false; 6861 6862 version(win32_widgets) { 6863 createWin32Window(this, WC_TABCONTROL, "", 0); 6864 } else version(custom_widgets) { 6865 addEventListener((ClickEvent event) { 6866 if(event.target !is this) 6867 return; 6868 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 6869 auto t = (event.clientX / tabWidth); 6870 if(t >= 0 && t < tabs.length) { 6871 currentTab_ = t; 6872 tabIndexClicked(t); 6873 redraw(); 6874 } 6875 } 6876 }); 6877 } else static assert(0); 6878 6879 this.container = makeContainer(); 6880 } 6881 6882 override int marginTop() { return 4; } 6883 override int paddingBottom() { return 4; } 6884 6885 override int minHeight() { 6886 int max = 0; 6887 foreach(child; children) 6888 max = mymax(child.minHeight, max); 6889 6890 6891 version(win32_widgets) { 6892 RECT rect; 6893 rect.right = this.width; 6894 rect.bottom = max; 6895 TabCtrl_AdjustRect(hwnd, true, &rect); 6896 6897 max = rect.bottom; 6898 } else { 6899 max += defaultLineHeight + 4; 6900 } 6901 6902 6903 return max; 6904 } 6905 6906 version(win32_widgets) 6907 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6908 switch(code) { 6909 case TCN_SELCHANGE: 6910 auto sel = TabCtrl_GetCurSel(hwnd); 6911 tabIndexClicked(sel); 6912 break; 6913 default: 6914 } 6915 return 0; 6916 } 6917 6918 version(custom_widgets) { 6919 private int currentTab_; 6920 private int tabBarHeight() { return defaultLineHeight; } 6921 int tabWidth() { return scaleWithDpi(80); } 6922 } 6923 6924 version(win32_widgets) 6925 override void paint(WidgetPainter painter) {} 6926 6927 version(custom_widgets) 6928 override void paint(WidgetPainter painter) { 6929 auto cs = getComputedStyle(); 6930 6931 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6932 6933 int posX = 0; 6934 foreach(idx, title; tabs) { 6935 auto isCurrent = idx == getCurrentTab(); 6936 6937 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6938 6939 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6940 painter.outlineColor = cs.foregroundColor; 6941 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6942 6943 if(isCurrent) { 6944 painter.outlineColor = cs.windowBackgroundColor; 6945 painter.fillColor = Color.transparent; 6946 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6947 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6948 6949 painter.outlineColor = Color.white; 6950 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6951 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6952 painter.outlineColor = cs.activeTabColor; 6953 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6954 } 6955 6956 posX += tabWidth - 2; 6957 } 6958 } 6959 6960 /// 6961 @scriptable 6962 void setCurrentTab(int item) { 6963 version(win32_widgets) 6964 TabCtrl_SetCurSel(hwnd, item); 6965 else version(custom_widgets) 6966 currentTab_ = item; 6967 else static assert(0); 6968 6969 tabIndexClicked(item); 6970 } 6971 6972 /// 6973 @scriptable 6974 int getCurrentTab() { 6975 version(win32_widgets) 6976 return TabCtrl_GetCurSel(hwnd); 6977 else version(custom_widgets) 6978 return currentTab_; // FIXME 6979 else static assert(0); 6980 } 6981 6982 /// 6983 @scriptable 6984 void removeTab(int item) { 6985 if(item && item == getCurrentTab()) 6986 setCurrentTab(item - 1); 6987 6988 version(win32_widgets) { 6989 TabCtrl_DeleteItem(hwnd, item); 6990 } 6991 6992 for(int a = item; a < children.length - 1; a++) 6993 this._children[a] = this._children[a + 1]; 6994 this._children = this._children[0 .. $-1]; 6995 } 6996 6997 } 6998 6999 7000 /++ 7001 A tab widget is a set of clickable tab buttons followed by a content area. 7002 7003 7004 Tabs can change existing content or can be new pages. 7005 7006 When the user picks a different tab, a `change` message is generated. 7007 +/ 7008 class TabWidget : TabMessageWidget { 7009 this(Widget parent) { 7010 super(parent); 7011 } 7012 7013 override protected Widget makeContainer() { 7014 return null; 7015 } 7016 7017 override void addChild(Widget child, int pos = int.max) { 7018 if(auto twp = cast(TabWidgetPage) child) { 7019 Widget.addChild(child, pos); 7020 if(pos == int.max) 7021 pos = cast(int) this.children.length - 1; 7022 7023 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7024 7025 if(pos != getCurrentTab) { 7026 child.showing = false; 7027 } 7028 } else { 7029 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7030 } 7031 } 7032 7033 // FIXME: add tab icons at some point, Windows supports them 7034 /++ 7035 Adds a page and its associated tab with the given label to the widget. 7036 7037 Returns: 7038 The added page object, to which you can add other widgets. 7039 +/ 7040 @scriptable 7041 TabWidgetPage addPage(string title) { 7042 return new TabWidgetPage(title, this); 7043 } 7044 7045 /++ 7046 Gets the page at the given tab index, or `null` if the index is bad. 7047 7048 History: 7049 Added December 24, 2021. 7050 +/ 7051 TabWidgetPage getPage(int index) { 7052 if(index < this.children.length) 7053 return null; 7054 return cast(TabWidgetPage) this.children[index]; 7055 } 7056 7057 /++ 7058 While you can still use the addTab from the parent class, 7059 *strongly* recommend you use [addPage] insteaad. 7060 7061 History: 7062 Added December 24, 2021 to fulful the interface 7063 requirement that came from adding [TabMessageWidget]. 7064 7065 You should not use it though since the [addPage] function 7066 is much easier to use here. 7067 +/ 7068 override int addTab(string title, int pos = int.max) { 7069 auto p = addPage(title); 7070 foreach(idx, child; this.children) 7071 if(child is p) 7072 return cast(int) idx; 7073 return -1; 7074 } 7075 7076 protected override void tabIndexClicked(int item) { 7077 foreach(idx, child; children) { 7078 child.showing(false, false); // batch the recalculates for the end 7079 } 7080 7081 foreach(idx, child; children) { 7082 if(idx == item) { 7083 child.showing(true, false); 7084 if(parentWindow) { 7085 auto f = parentWindow.getFirstFocusable(child); 7086 if(f) 7087 f.focus(); 7088 } 7089 recomputeChildLayout(); 7090 } 7091 } 7092 7093 version(win32_widgets) { 7094 InvalidateRect(hwnd, null, true); 7095 } else version(custom_widgets) { 7096 this.redraw(); 7097 } 7098 } 7099 7100 } 7101 7102 /++ 7103 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7104 7105 You add [TabWidgetPage]s to it. 7106 +/ 7107 class PageWidget : Widget { 7108 this(Widget parent) { 7109 super(parent); 7110 } 7111 7112 override int minHeight() { 7113 int max = 0; 7114 foreach(child; children) 7115 max = mymax(child.minHeight, max); 7116 7117 return max; 7118 } 7119 7120 7121 override void addChild(Widget child, int pos = int.max) { 7122 if(auto twp = cast(TabWidgetPage) child) { 7123 super.addChild(child, pos); 7124 if(pos == int.max) 7125 pos = cast(int) this.children.length - 1; 7126 7127 if(pos != getCurrentTab) { 7128 child.showing = false; 7129 } 7130 } else { 7131 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7132 } 7133 } 7134 7135 override void recomputeChildLayout() { 7136 this.registerMovement(); 7137 foreach(child; children) { 7138 child.x = 0; 7139 child.y = 0; 7140 child.width = width; 7141 child.height = height; 7142 child.recomputeChildLayout(); 7143 } 7144 } 7145 7146 private int currentTab_; 7147 7148 /// 7149 @scriptable 7150 void setCurrentTab(int item) { 7151 currentTab_ = item; 7152 7153 showOnly(item); 7154 } 7155 7156 /// 7157 @scriptable 7158 int getCurrentTab() { 7159 return currentTab_; 7160 } 7161 7162 /// 7163 @scriptable 7164 void removeTab(int item) { 7165 if(item && item == getCurrentTab()) 7166 setCurrentTab(item - 1); 7167 7168 for(int a = item; a < children.length - 1; a++) 7169 this._children[a] = this._children[a + 1]; 7170 this._children = this._children[0 .. $-1]; 7171 } 7172 7173 /// 7174 @scriptable 7175 TabWidgetPage addPage(string title) { 7176 return new TabWidgetPage(title, this); 7177 } 7178 7179 private void showOnly(int item) { 7180 foreach(idx, child; children) 7181 if(idx == item) { 7182 child.show(); 7183 child.queueRecomputeChildLayout(); 7184 } else { 7185 child.hide(); 7186 } 7187 } 7188 7189 } 7190 7191 /++ 7192 7193 +/ 7194 class TabWidgetPage : Widget { 7195 string title; 7196 this(string title, Widget parent) { 7197 this.title = title; 7198 this.tabStop = false; 7199 super(parent); 7200 7201 ///* 7202 version(win32_widgets) { 7203 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7204 } 7205 //*/ 7206 } 7207 7208 override int minHeight() { 7209 int sum = 0; 7210 foreach(child; children) 7211 sum += child.minHeight(); 7212 return sum; 7213 } 7214 } 7215 7216 version(none) 7217 /++ 7218 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7219 7220 I think I need to modify the layout algorithms to support this. 7221 +/ 7222 class CollapsableSidebar : Widget { 7223 7224 } 7225 7226 /// Stacks the widgets vertically, taking all the available width for each child. 7227 class VerticalLayout : Layout { 7228 // most of this is intentionally blank - widget's default is vertical layout right now 7229 /// 7230 this(Widget parent) { super(parent); } 7231 7232 /++ 7233 Sets a max width for the layout so you don't have to subclass. The max width 7234 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7235 7236 History: 7237 Added November 29, 2021 (dub v10.5) 7238 +/ 7239 this(int maxWidth, Widget parent) { 7240 this.mw = maxWidth; 7241 super(parent); 7242 } 7243 7244 private int mw = int.max; 7245 7246 override int maxWidth() { return scaleWithDpi(mw); } 7247 } 7248 7249 /// Stacks the widgets horizontally, taking all the available height for each child. 7250 class HorizontalLayout : Layout { 7251 /// 7252 this(Widget parent) { super(parent); } 7253 7254 /++ 7255 Sets a max height for the layout so you don't have to subclass. The max height 7256 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7257 7258 History: 7259 Added November 29, 2021 (dub v10.5) 7260 +/ 7261 this(int maxHeight, Widget parent) { 7262 this.mh = maxHeight; 7263 super(parent); 7264 } 7265 7266 private int mh = 0; 7267 7268 7269 7270 override void recomputeChildLayout() { 7271 .recomputeChildLayout!"width"(this); 7272 } 7273 7274 override int minHeight() { 7275 int largest = 0; 7276 int margins = 0; 7277 int lastMargin = 0; 7278 foreach(child; children) { 7279 auto mh = child.minHeight(); 7280 if(mh > largest) 7281 largest = mh; 7282 margins += mymax(lastMargin, child.marginTop()); 7283 lastMargin = child.marginBottom(); 7284 } 7285 return largest + margins; 7286 } 7287 7288 override int maxHeight() { 7289 if(mh != 0) 7290 return mymax(minHeight, scaleWithDpi(mh)); 7291 7292 int largest = 0; 7293 int margins = 0; 7294 int lastMargin = 0; 7295 foreach(child; children) { 7296 auto mh = child.maxHeight(); 7297 if(mh == int.max) 7298 return int.max; 7299 if(mh > largest) 7300 largest = mh; 7301 margins += mymax(lastMargin, child.marginTop()); 7302 lastMargin = child.marginBottom(); 7303 } 7304 return largest + margins; 7305 } 7306 7307 override int heightStretchiness() { 7308 int max; 7309 foreach(child; children) { 7310 auto c = child.heightStretchiness; 7311 if(c > max) 7312 max = c; 7313 } 7314 return max; 7315 } 7316 7317 } 7318 7319 version(win32_widgets) 7320 private 7321 extern(Windows) 7322 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7323 Widget* pwin = hwnd in Widget.nativeMapping; 7324 if(pwin is null) 7325 return DefWindowProc(hwnd, message, wparam, lparam); 7326 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7327 if(win is null) 7328 return DefWindowProc(hwnd, message, wparam, lparam); 7329 7330 switch(message) { 7331 case WM_SIZE: 7332 auto width = LOWORD(lparam); 7333 auto height = HIWORD(lparam); 7334 7335 auto hdc = GetDC(hwnd); 7336 auto hdcBmp = CreateCompatibleDC(hdc); 7337 7338 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7339 if(width > win.bmpWidth || height > win.bmpHeight) { 7340 auto oldBuffer = win.buffer; 7341 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7342 7343 if(oldBuffer) 7344 DeleteObject(oldBuffer); 7345 7346 win.bmpWidth = width; 7347 win.bmpHeight = height; 7348 } 7349 7350 // just always erase it upon resizing so minigui can draw over with a clean slate 7351 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7352 7353 auto brush = GetSysColorBrush(COLOR_3DFACE); 7354 RECT r; 7355 r.left = 0; 7356 r.top = 0; 7357 r.right = width; 7358 r.bottom = height; 7359 FillRect(hdcBmp, &r, brush); 7360 7361 SelectObject(hdcBmp, oldBmp); 7362 DeleteDC(hdcBmp); 7363 ReleaseDC(hwnd, hdc); 7364 break; 7365 case WM_PAINT: 7366 if(win.buffer is null) 7367 goto default; 7368 7369 BITMAP bm; 7370 PAINTSTRUCT ps; 7371 7372 HDC hdc = BeginPaint(hwnd, &ps); 7373 7374 HDC hdcMem = CreateCompatibleDC(hdc); 7375 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7376 7377 GetObject(win.buffer, bm.sizeof, &bm); 7378 7379 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7380 7381 SelectObject(hdcMem, hbmOld); 7382 DeleteDC(hdcMem); 7383 EndPaint(hwnd, &ps); 7384 break; 7385 default: 7386 return DefWindowProc(hwnd, message, wparam, lparam); 7387 } 7388 7389 return 0; 7390 } 7391 7392 private wstring Win32Class(wstring name)() { 7393 static bool classRegistered; 7394 if(!classRegistered) { 7395 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7396 WNDCLASSEX wc; 7397 wc.cbSize = wc.sizeof; 7398 wc.hInstance = hInstance; 7399 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7400 wc.lpfnWndProc = &DoubleBufferWndProc; 7401 wc.lpszClassName = name.ptr; 7402 if(!RegisterClassExW(&wc)) 7403 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7404 classRegistered = true; 7405 } 7406 7407 return name; 7408 } 7409 7410 /+ 7411 version(win32_widgets) 7412 extern(Windows) 7413 private 7414 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7415 switch(iMessage) { 7416 case WM_PAINT: 7417 if(auto te = hWnd in Widget.nativeMapping) { 7418 try { 7419 //te.redraw(); 7420 writeln(te, " drawing"); 7421 } catch(Exception) {} 7422 } 7423 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7424 default: 7425 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7426 } 7427 } 7428 +/ 7429 7430 7431 /++ 7432 A widget specifically designed to hold other widgets. 7433 7434 History: 7435 Added July 1, 2021 7436 +/ 7437 class ContainerWidget : Widget { 7438 this(Widget parent) { 7439 super(parent); 7440 this.tabStop = false; 7441 7442 version(win32_widgets) { 7443 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7444 } 7445 } 7446 } 7447 7448 /++ 7449 A widget that takes your widget, puts scroll bars around it, and sends 7450 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7451 no effort to automatically scroll or clip its child widgets - it just sends 7452 the messages. 7453 7454 7455 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7456 The scroll coordinates are all given in a unit you interpret as you wish. One 7457 of these units is moved on each press of the arrow buttons and represents the 7458 smallest amount the user can scroll. The intention is for this to be one line, 7459 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7460 in each direction that the user might be interested in. 7461 7462 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7463 This is the amount it jumps when the user pressed page up and page down, or clicks 7464 in the exposed part of the scroll bar. 7465 7466 You should add child content to the ScrollMessageWidget. However, it is important to 7467 note that the coordinates are always independent of the scroll position! It is YOUR 7468 responsibility to do any necessary transforms, clipping, etc., while drawing the 7469 content and interpreting mouse events if they are supposed to change with the scroll. 7470 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7471 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7472 you more control (which can be considerably more efficient and adapted to your actual data) 7473 at the expense of you also needing to be aware of its reality. 7474 7475 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7476 version 10.3. Maybe this will change in the future.... but for now you must call 7477 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7478 +/ 7479 class ScrollMessageWidget : Widget { 7480 this(Widget parent) { 7481 super(parent); 7482 7483 container = new Widget(this); 7484 hsb = new HorizontalScrollbar(this); 7485 vsb = new VerticalScrollbar(this); 7486 7487 hsb.addEventListener("scrolltonextline", { 7488 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7489 notify(); 7490 }); 7491 hsb.addEventListener("scrolltopreviousline", { 7492 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7493 notify(); 7494 }); 7495 vsb.addEventListener("scrolltonextline", { 7496 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7497 notify(); 7498 }); 7499 vsb.addEventListener("scrolltopreviousline", { 7500 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7501 notify(); 7502 }); 7503 hsb.addEventListener("scrolltonextpage", { 7504 hsb.setPosition(hsb.position + hsb.step_); 7505 notify(); 7506 }); 7507 hsb.addEventListener("scrolltopreviouspage", { 7508 hsb.setPosition(hsb.position - hsb.step_); 7509 notify(); 7510 }); 7511 vsb.addEventListener("scrolltonextpage", { 7512 vsb.setPosition(vsb.position + vsb.step_); 7513 notify(); 7514 }); 7515 vsb.addEventListener("scrolltopreviouspage", { 7516 vsb.setPosition(vsb.position - vsb.step_); 7517 notify(); 7518 }); 7519 hsb.addEventListener("scrolltoposition", (Event event) { 7520 hsb.setPosition(event.intValue); 7521 notify(); 7522 }); 7523 vsb.addEventListener("scrolltoposition", (Event event) { 7524 vsb.setPosition(event.intValue); 7525 notify(); 7526 }); 7527 7528 7529 tabStop = false; 7530 container.tabStop = false; 7531 magic = true; 7532 } 7533 7534 private int movementPerButtonClickH_ = 1; 7535 private int movementPerButtonClickV_ = 1; 7536 public void movementPerButtonClick(int h, int v) { 7537 movementPerButtonClickH_ = h; 7538 movementPerButtonClickV_ = v; 7539 } 7540 7541 /++ 7542 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7543 7544 7545 The defaults for [addDefaultWheelListeners] are: 7546 7547 $(LIST 7548 * Mouse wheel scrolls vertically 7549 * Alt key + mouse wheel scrolls horiontally 7550 * Shift + mouse wheel scrolls faster. 7551 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7552 ) 7553 7554 The defaults for [addDefaultKeyboardListeners] are: 7555 7556 $(LIST 7557 * Arrow keys scroll by the given amounts 7558 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7559 * Page up and down scroll by the vertical viewable area 7560 * Home and end scroll to the start and end of the verticle viewable area. 7561 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7562 ) 7563 7564 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7565 7566 Params: 7567 horizontalArrowScrollAmount = 7568 verticalArrowScrollAmount = 7569 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7570 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7571 shiftMultiplier = multiplies the scroll amount by this when shift is held 7572 +/ 7573 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7574 auto _this = this; 7575 7576 container.addEventListener((scope KeyDownEvent ke) { 7577 switch(ke.key) { 7578 case Key.Left: 7579 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7580 break; 7581 case Key.Right: 7582 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7583 break; 7584 case Key.Up: 7585 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7586 break; 7587 case Key.Down: 7588 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7589 break; 7590 case Key.PageUp: 7591 if(ke.altKey) 7592 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7593 else 7594 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7595 break; 7596 case Key.PageDown: 7597 if(ke.altKey) 7598 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7599 else 7600 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7601 break; 7602 case Key.Home: 7603 if(ke.altKey) 7604 _this.scrollLeft(short.max * 16); 7605 else 7606 _this.scrollUp(short.max * 16); 7607 break; 7608 case Key.End: 7609 if(ke.altKey) 7610 _this.scrollRight(short.max * 16); 7611 else 7612 _this.scrollDown(short.max * 16); 7613 break; 7614 7615 default: 7616 // ignore, not for us. 7617 } 7618 7619 }); 7620 } 7621 7622 /// ditto 7623 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7624 auto _this = this; 7625 container.addEventListener((scope ClickEvent ce) { 7626 7627 //if(ce.target && ce.target.tabStop) 7628 //ce.target.focus(); 7629 7630 // ctrl is reserved for the application 7631 if(ce.ctrlKey) 7632 return; 7633 7634 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7635 return; 7636 7637 if(shiftMultiplier == 0 && ce.shiftKey) 7638 return; 7639 7640 if(ce.button == MouseButton.wheelDown) { 7641 if(ce.altKey) 7642 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7643 else 7644 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7645 } else if(ce.button == MouseButton.wheelUp) { 7646 if(ce.altKey) 7647 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7648 else 7649 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7650 } 7651 }); 7652 } 7653 7654 /++ 7655 Scrolls the given amount. 7656 7657 History: 7658 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. 7659 +/ 7660 void scrollUp(int amount = 1) { 7661 vsb.setPosition(vsb.position - amount); 7662 notify(); 7663 } 7664 /// ditto 7665 void scrollDown(int amount = 1) { 7666 vsb.setPosition(vsb.position + amount); 7667 notify(); 7668 } 7669 /// ditto 7670 void scrollLeft(int amount = 1) { 7671 hsb.setPosition(hsb.position - amount); 7672 notify(); 7673 } 7674 /// ditto 7675 void scrollRight(int amount = 1) { 7676 hsb.setPosition(hsb.position + amount); 7677 notify(); 7678 } 7679 7680 /// 7681 VerticalScrollbar verticalScrollBar() { return vsb; } 7682 /// 7683 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7684 7685 void notify() { 7686 static bool insideNotify; 7687 7688 if(insideNotify) 7689 return; // avoid the recursive call, even if it isn't strictly correct 7690 7691 insideNotify = true; 7692 scope(exit) insideNotify = false; 7693 7694 this.emit!ScrollEvent(); 7695 } 7696 7697 mixin Emits!ScrollEvent; 7698 7699 /// 7700 Point position() { 7701 return Point(hsb.position, vsb.position); 7702 } 7703 7704 /// 7705 void setPosition(int x, int y) { 7706 hsb.setPosition(x); 7707 vsb.setPosition(y); 7708 } 7709 7710 /// 7711 void setPageSize(int unitsX, int unitsY) { 7712 hsb.setStep(unitsX); 7713 vsb.setStep(unitsY); 7714 } 7715 7716 /// Always call this BEFORE setViewableArea 7717 void setTotalArea(int width, int height) { 7718 hsb.setMax(width); 7719 vsb.setMax(height); 7720 } 7721 7722 /++ 7723 Always set the viewable area AFTER setitng the total area if you are going to change both. 7724 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7725 If you need to do that, use [queueRecomputeChildLayout]. 7726 +/ 7727 void setViewableArea(int width, int height) { 7728 7729 // actually there IS A need to dothis cuz the max might have changed since then 7730 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7731 //return; // no need to do what is already done 7732 hsb.setViewableArea(width); 7733 vsb.setViewableArea(height); 7734 7735 bool needsNotify = false; 7736 7737 // FIXME: if at any point the rhs is outside the scrollbar, we need 7738 // to reset to 0. but it should remember the old position in case the 7739 // window resizes again, so it can kinda return ot where it was. 7740 // 7741 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7742 if(width >= hsb.max) { 7743 // there's plenty of room to display it all so we need to reset to zero 7744 // FIXME: adjust so it matches the note above 7745 hsb.setPosition(0); 7746 needsNotify = true; 7747 } 7748 if(height >= vsb.max) { 7749 // there's plenty of room to display it all so we need to reset to zero 7750 // FIXME: adjust so it matches the note above 7751 vsb.setPosition(0); 7752 needsNotify = true; 7753 } 7754 if(needsNotify) 7755 notify(); 7756 } 7757 7758 private bool magic; 7759 override void addChild(Widget w, int position = int.max) { 7760 if(magic) 7761 container.addChild(w, position); 7762 else 7763 super.addChild(w, position); 7764 } 7765 7766 override void recomputeChildLayout() { 7767 if(hsb is null || vsb is null || container is null) return; 7768 7769 registerMovement(); 7770 7771 enum BUTTON_SIZE = 16; 7772 7773 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7774 hsb.x = 0; 7775 hsb.y = this.height - hsb.height; 7776 7777 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7778 vsb.x = this.width - vsb.width; 7779 vsb.y = 0; 7780 7781 auto vsb_width = vsb.showing ? vsb.width : 0; 7782 auto hsb_height = hsb.showing ? hsb.height : 0; 7783 7784 hsb.width = this.width - vsb_width; 7785 vsb.height = this.height - hsb_height; 7786 7787 hsb.recomputeChildLayout(); 7788 vsb.recomputeChildLayout(); 7789 7790 if(this.header is null) { 7791 container.x = 0; 7792 container.y = 0; 7793 container.width = this.width - vsb_width; 7794 container.height = this.height - hsb_height; 7795 container.recomputeChildLayout(); 7796 } else { 7797 header.x = 0; 7798 header.y = 0; 7799 header.width = this.width - vsb_width; 7800 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7801 header.recomputeChildLayout(); 7802 7803 container.x = 0; 7804 container.y = scaleWithDpi(BUTTON_SIZE); 7805 container.width = this.width - vsb_width; 7806 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7807 container.recomputeChildLayout(); 7808 } 7809 } 7810 7811 private HorizontalScrollbar hsb; 7812 private VerticalScrollbar vsb; 7813 Widget container; 7814 private Widget header; 7815 7816 /++ 7817 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7818 7819 History: 7820 Added September 27, 2021 (dub v10.3) 7821 +/ 7822 Widget getHeader() { 7823 if(this.header is null) { 7824 magic = false; 7825 scope(exit) magic = true; 7826 this.header = new Widget(this); 7827 queueRecomputeChildLayout(); 7828 } 7829 return this.header; 7830 } 7831 7832 /++ 7833 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7834 7835 History: 7836 Added January 3, 2023 (dub v11.0) 7837 +/ 7838 void scrollIntoView(Rectangle rect) { 7839 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7840 7841 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7842 7843 // the lower right is exclusive normally 7844 auto test = rect.lowerRight; 7845 if(test.x > 0) test.x--; 7846 if(test.y > 0) test.y--; 7847 7848 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7849 // try to scroll only one dimension at a time if we can 7850 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7851 setPosition(rect.upperLeft.x, position.y); 7852 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7853 setPosition(position.x, rect.upperLeft.y); 7854 } 7855 7856 } 7857 7858 override int minHeight() { 7859 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 7860 if(header !is null) 7861 min += header.minHeight; 7862 if(horizontalScrollBar.showing) 7863 min += horizontalScrollBar.minHeight; 7864 return min; 7865 } 7866 7867 override int maxHeight() { 7868 int max = container ? container.maxHeight : int.max; 7869 if(max == int.max) 7870 return max; 7871 if(horizontalScrollBar.showing) 7872 max += horizontalScrollBar.minHeight; 7873 return max; 7874 } 7875 } 7876 7877 /++ 7878 $(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") 7879 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7880 +/ 7881 version(minigui_screenshots) 7882 @Screenshot("ScrollMessageWidget") 7883 unittest { 7884 auto window = new Window("ScrollMessageWidget"); 7885 7886 auto smw = new ScrollMessageWidget(window); 7887 smw.addDefaultKeyboardListeners(); 7888 smw.addDefaultWheelListeners(); 7889 7890 window.loop(); 7891 } 7892 7893 /++ 7894 Bypasses automatic layout for its children, using manual positioning and sizing only. 7895 While you need to manually position them, you must ensure they are inside the StaticLayout's 7896 bounding box to avoid undefined behavior. 7897 7898 You should almost never use this. 7899 +/ 7900 class StaticLayout : Layout { 7901 /// 7902 this(Widget parent) { super(parent); } 7903 override void recomputeChildLayout() { 7904 registerMovement(); 7905 foreach(child; children) 7906 child.recomputeChildLayout(); 7907 } 7908 } 7909 7910 /++ 7911 Bypasses automatic positioning when being laid out. It is your responsibility to make 7912 room for this widget in the parent layout. 7913 7914 Its children are laid out normally, unless there is exactly one, in which case it takes 7915 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7916 can do that with `padding`). 7917 +/ 7918 class StaticPosition : Layout { 7919 /// 7920 this(Widget parent) { super(parent); } 7921 7922 override void recomputeChildLayout() { 7923 registerMovement(); 7924 if(this.children.length == 1) { 7925 auto child = children[0]; 7926 child.x = 0; 7927 child.y = 0; 7928 child.width = this.width; 7929 child.height = this.height; 7930 child.recomputeChildLayout(); 7931 } else 7932 foreach(child; children) 7933 child.recomputeChildLayout(); 7934 } 7935 7936 alias width = typeof(super).width; 7937 alias height = typeof(super).height; 7938 7939 @property int width(int w) @nogc pure @safe nothrow { 7940 return this._width = w; 7941 } 7942 7943 @property int height(int w) @nogc pure @safe nothrow { 7944 return this._height = w; 7945 } 7946 7947 } 7948 7949 /++ 7950 FixedPosition is like [StaticPosition], but its coordinates 7951 are always relative to the viewport, meaning they do not scroll with 7952 the parent content. 7953 +/ 7954 class FixedPosition : StaticPosition { 7955 /// 7956 this(Widget parent) { super(parent); } 7957 } 7958 7959 version(win32_widgets) 7960 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7961 if(true) { 7962 // cmd == 0 = menu, cmd == 1 = accelerator 7963 if(auto item = idm in Action.mapping) { 7964 foreach(handler; (*item).triggered) 7965 handler(); 7966 /* 7967 auto event = new Event("triggered", *item); 7968 event.button = idm; 7969 event.dispatch(); 7970 */ 7971 return 0; 7972 } 7973 } 7974 if(handle) 7975 if(auto widgetp = handle in Widget.nativeMapping) { 7976 (*widgetp).handleWmCommand(cmd, idm); 7977 return 0; 7978 } 7979 return 1; 7980 } 7981 7982 7983 /// 7984 class Window : Widget { 7985 int mouseCaptureCount = 0; 7986 Widget mouseCapturedBy; 7987 void captureMouse(Widget byWhom) { 7988 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7989 mouseCaptureCount++; 7990 mouseCapturedBy = byWhom; 7991 win.grabInput(); 7992 } 7993 void releaseMouseCapture() { 7994 mouseCaptureCount--; 7995 mouseCapturedBy = null; 7996 win.releaseInputGrab(); 7997 } 7998 7999 /++ 8000 Sets the window icon which is often seen in title bars and taskbars. 8001 8002 History: 8003 Added April 5, 2022 (dub v10.8) 8004 +/ 8005 @property void icon(MemoryImage icon) { 8006 if(win && icon) 8007 win.icon = icon; 8008 } 8009 8010 // forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples 8011 // this does NOT change the icon on the window! That's what the other overload is for 8012 static @property .icon icon(GenericIcons i) { 8013 return .icon(i); 8014 } 8015 8016 /// 8017 @scriptable 8018 @property bool focused() { 8019 return win.focused; 8020 } 8021 8022 static class Style : Widget.Style { 8023 override WidgetBackground background() { 8024 version(custom_widgets) 8025 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8026 else version(win32_widgets) 8027 return WidgetBackground(Color.transparent); 8028 else static assert(0); 8029 } 8030 } 8031 mixin OverrideStyle!Style; 8032 8033 /++ 8034 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. 8035 +/ 8036 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8037 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8038 } 8039 8040 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8041 OperatingSystemFont font; 8042 if(auto vt = WidgetPainter.visualTheme) { 8043 font = vt.defaultFontCached(96); // FIXME 8044 } 8045 8046 if(font is null) { 8047 static int defaultHeightCache; 8048 if(defaultHeightCache == 0) { 8049 font = new OperatingSystemFont; 8050 font.loadDefault; 8051 defaultHeightCache = font.height();// * 5 / 4; 8052 } 8053 return defaultHeightCache; 8054 } 8055 8056 return font.height();// * 5 / 4; 8057 } 8058 8059 Widget focusedWidget; 8060 8061 private SimpleWindow win_; 8062 8063 @property { 8064 /++ 8065 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8066 8067 History: 8068 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8069 +/ 8070 public SimpleWindow win() { 8071 return win_; 8072 } 8073 /// 8074 protected void win(SimpleWindow w) { 8075 win_ = w; 8076 } 8077 } 8078 8079 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8080 this(Widget p) { 8081 tabStop = false; 8082 super(p); 8083 } 8084 8085 private void actualRedraw() { 8086 if(recomputeChildLayoutRequired) 8087 recomputeChildLayoutEntry(); 8088 if(!showing) return; 8089 8090 assert(parentWindow !is null); 8091 8092 auto w = drawableWindow; 8093 if(w is null) 8094 w = parentWindow.win; 8095 8096 if(w.closed()) 8097 return; 8098 8099 auto ugh = this.parent; 8100 int lox, loy; 8101 while(ugh) { 8102 lox += ugh.x; 8103 loy += ugh.y; 8104 ugh = ugh.parent; 8105 } 8106 auto painter = w.draw(true); 8107 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8108 } 8109 8110 8111 private bool skipNextChar = false; 8112 8113 /++ 8114 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8115 8116 This constructor is intended primarily for internal use and may be changed to `protected` later. 8117 +/ 8118 this(SimpleWindow win) { 8119 8120 static if(UsingSimpledisplayX11) { 8121 win.discardAdditionalConnectionState = &discardXConnectionState; 8122 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8123 } 8124 8125 tabStop = false; 8126 super(null); 8127 this.win = win; 8128 8129 win.addEventListener((Widget.RedrawEvent) { 8130 if(win.eventQueued!RecomputeEvent) { 8131 // writeln("skipping"); 8132 return; // let the recompute event do the actual redraw 8133 } 8134 this.actualRedraw(); 8135 }); 8136 8137 win.addEventListener((Widget.RecomputeEvent) { 8138 recomputeChildLayoutEntry(); 8139 if(win.eventQueued!RedrawEvent) 8140 return; // let the queued one do it 8141 else { 8142 // writeln("drawing"); 8143 this.actualRedraw(); // if not queued, it needs to be done now anyway 8144 } 8145 }); 8146 8147 this.width = win.width; 8148 this.height = win.height; 8149 this.parentWindow = this; 8150 8151 win.closeQuery = () { 8152 if(this.emit!ClosingEvent()) 8153 win.close(); 8154 }; 8155 win.onClosing = () { 8156 this.emit!ClosedEvent(); 8157 }; 8158 8159 win.windowResized = (int w, int h) { 8160 this.width = w; 8161 this.height = h; 8162 queueRecomputeChildLayout(); 8163 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8164 //version(win32_widgets) 8165 //InvalidateRect(hwnd, null, true); 8166 redraw(); 8167 }; 8168 8169 win.onFocusChange = (bool getting) { 8170 if(this.focusedWidget) { 8171 if(getting) { 8172 this.focusedWidget.emit!FocusEvent(); 8173 this.focusedWidget.emit!FocusInEvent(); 8174 } else { 8175 this.focusedWidget.emit!BlurEvent(); 8176 this.focusedWidget.emit!FocusOutEvent(); 8177 } 8178 } 8179 8180 if(getting) { 8181 this.emit!FocusEvent(); 8182 this.emit!FocusInEvent(); 8183 } else { 8184 this.emit!BlurEvent(); 8185 this.emit!FocusOutEvent(); 8186 } 8187 }; 8188 8189 win.onDpiChanged = { 8190 this.queueRecomputeChildLayout(); 8191 auto event = new DpiChangedEvent(this); 8192 event.sendDirectly(); 8193 8194 privateDpiChanged(); 8195 }; 8196 8197 win.setEventHandlers( 8198 (MouseEvent e) { 8199 dispatchMouseEvent(e); 8200 }, 8201 (KeyEvent e) { 8202 //writefln("%x %s", cast(uint) e.key, e.key); 8203 dispatchKeyEvent(e); 8204 }, 8205 (dchar e) { 8206 if(e == 13) e = 10; // hack? 8207 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8208 dispatchCharEvent(e); 8209 }, 8210 ); 8211 8212 addEventListener("char", (Widget, Event ev) { 8213 if(skipNextChar) { 8214 ev.preventDefault(); 8215 skipNextChar = false; 8216 } 8217 }); 8218 8219 version(win32_widgets) 8220 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8221 if(hwnd !is this.win.impl.hwnd) 8222 return 1; // we don't care... pass it on 8223 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8224 if(mustReturn) 8225 return ret; 8226 return 1; // pass it on 8227 }; 8228 8229 if(Window.newWindowCreated) 8230 Window.newWindowCreated(this); 8231 } 8232 8233 version(custom_widgets) 8234 override void defaultEventHandler_click(ClickEvent event) { 8235 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 8236 if(event.target && event.target.tabStop) 8237 event.target.focus(); 8238 } 8239 } 8240 8241 private static void delegate(Window) newWindowCreated; 8242 8243 version(win32_widgets) 8244 override void paint(WidgetPainter painter) { 8245 /* 8246 RECT rect; 8247 rect.right = this.width; 8248 rect.bottom = this.height; 8249 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8250 */ 8251 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8252 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8253 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8254 // since the pen is null, to fill the whole space, we need the +1 on both. 8255 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8256 SelectObject(painter.impl.hdc, p); 8257 SelectObject(painter.impl.hdc, b); 8258 } 8259 version(custom_widgets) 8260 override void paint(WidgetPainter painter) { 8261 auto cs = getComputedStyle(); 8262 painter.fillColor = cs.windowBackgroundColor; 8263 painter.outlineColor = cs.windowBackgroundColor; 8264 painter.drawRectangle(Point(0, 0), this.width, this.height); 8265 } 8266 8267 8268 override void defaultEventHandler_keydown(KeyDownEvent event) { 8269 Widget _this = event.target; 8270 8271 if(event.key == Key.Tab) { 8272 /* Window tab ordering is a recursive thingy with each group */ 8273 8274 // FIXME inefficient 8275 Widget[] helper(Widget p) { 8276 if(p.hidden) 8277 return null; 8278 Widget[] childOrdering; 8279 8280 auto children = p.children.dup; 8281 8282 while(true) { 8283 // UIs should be generally small, so gonna brute force it a little 8284 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8285 8286 Widget smallestTab; 8287 foreach(ref c; children) { 8288 if(c is null) continue; 8289 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8290 smallestTab = c; 8291 c = null; 8292 } 8293 } 8294 if(smallestTab !is null) { 8295 if(smallestTab.tabStop && !smallestTab.hidden) 8296 childOrdering ~= smallestTab; 8297 if(!smallestTab.hidden) 8298 childOrdering ~= helper(smallestTab); 8299 } else 8300 break; 8301 8302 } 8303 8304 return childOrdering; 8305 } 8306 8307 Widget[] tabOrdering = helper(this); 8308 8309 Widget recipient; 8310 8311 if(tabOrdering.length) { 8312 bool seenThis = false; 8313 Widget previous; 8314 foreach(idx, child; tabOrdering) { 8315 if(child is focusedWidget) { 8316 8317 if(event.shiftKey) { 8318 if(idx == 0) 8319 recipient = tabOrdering[$-1]; 8320 else 8321 recipient = tabOrdering[idx - 1]; 8322 break; 8323 } 8324 8325 seenThis = true; 8326 if(idx + 1 == tabOrdering.length) { 8327 // we're at the end, either move to the next group 8328 // or start back over 8329 recipient = tabOrdering[0]; 8330 } 8331 continue; 8332 } 8333 if(seenThis) { 8334 recipient = child; 8335 break; 8336 } 8337 previous = child; 8338 } 8339 } 8340 8341 if(recipient !is null) { 8342 // writeln(typeid(recipient)); 8343 recipient.focus(); 8344 8345 skipNextChar = true; 8346 } 8347 } 8348 8349 debug if(event.key == Key.F12) { 8350 if(devTools) { 8351 devTools.close(); 8352 devTools = null; 8353 } else { 8354 devTools = new DevToolWindow(this); 8355 devTools.show(); 8356 } 8357 } 8358 } 8359 8360 debug DevToolWindow devTools; 8361 8362 8363 /++ 8364 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8365 8366 History: 8367 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8368 8369 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8370 +/ 8371 this(int width = 500, int height = 500, string title = null) { 8372 if(title is null) { 8373 import core.runtime; 8374 if(Runtime.args.length) 8375 title = Runtime.args[0]; 8376 } 8377 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8378 8379 static if(UsingSimpledisplayX11) { 8380 ///+ 8381 // for input proxy 8382 auto display = XDisplayConnection.get; 8383 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8384 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8385 XMapWindow(display, inputProxy); 8386 // writefln("input proxy: 0x%0x", inputProxy); 8387 this.inputProxy = new SimpleWindow(inputProxy); 8388 8389 XEvent lastEvent; 8390 this.inputProxy.handleNativeEvent = (XEvent ev) { 8391 lastEvent = ev; 8392 return 1; 8393 }; 8394 this.inputProxy.setEventHandlers( 8395 (MouseEvent e) { 8396 dispatchMouseEvent(e); 8397 }, 8398 (KeyEvent e) { 8399 //writefln("%x %s", cast(uint) e.key, e.key); 8400 if(dispatchKeyEvent(e)) { 8401 // FIXME: i should trap error 8402 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8403 auto thing = nw.focusableWindow(); 8404 if(thing && thing.window) { 8405 lastEvent.xkey.window = thing.window; 8406 // writeln("sending event ", lastEvent.xkey); 8407 trapXErrors( { 8408 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8409 }); 8410 } 8411 } 8412 } 8413 }, 8414 (dchar e) { 8415 if(e == 13) e = 10; // hack? 8416 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8417 dispatchCharEvent(e); 8418 }, 8419 ); 8420 8421 this.inputProxy.populateXic(); 8422 // done 8423 //+/ 8424 } 8425 8426 8427 8428 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8429 8430 this(win); 8431 } 8432 8433 SimpleWindow inputProxy; 8434 8435 private SimpleWindow setRequestedInputFocus() { 8436 return inputProxy; 8437 } 8438 8439 /// ditto 8440 this(string title, int width = 500, int height = 500) { 8441 this(width, height, title); 8442 } 8443 8444 /// 8445 @property string title() { return parentWindow.win.title; } 8446 /// 8447 @property void title(string title) { parentWindow.win.title = title; } 8448 8449 /// 8450 @scriptable 8451 void close() { 8452 win.close(); 8453 // I synchronize here upon window closing to ensure all child windows 8454 // get updated too before the event loop. This avoids some random X errors. 8455 static if(UsingSimpledisplayX11) { 8456 runInGuiThread( { 8457 XSync(XDisplayConnection.get, false); 8458 }); 8459 } 8460 } 8461 8462 bool dispatchKeyEvent(KeyEvent ev) { 8463 auto wid = focusedWidget; 8464 if(wid is null) 8465 wid = this; 8466 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8467 event.originalKeyEvent = ev; 8468 event.key = ev.key; 8469 event.state = ev.modifierState; 8470 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8471 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8472 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8473 event.dispatch(); 8474 8475 return !event.propagationStopped; 8476 } 8477 8478 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8479 bool dispatchCharEvent(dchar ch) { 8480 if(focusedWidget) { 8481 auto event = new CharEvent(focusedWidget, ch); 8482 event.dispatch(); 8483 return !event.propagationStopped; 8484 } 8485 return true; 8486 } 8487 8488 Widget mouseLastOver; 8489 Widget mouseLastDownOn; 8490 bool lastWasDoubleClick; 8491 bool dispatchMouseEvent(MouseEvent ev) { 8492 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8493 auto ele = eleR.widget; 8494 8495 auto captureEle = ele; 8496 8497 if(mouseCapturedBy !is null) { 8498 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8499 captureEle = mouseCapturedBy; 8500 } 8501 8502 // a hack to get it relative to the widget. 8503 eleR.x = ev.x; 8504 eleR.y = ev.y; 8505 auto pain = captureEle; 8506 while(pain) { 8507 eleR.x -= pain.x; 8508 eleR.y -= pain.y; 8509 pain.addScrollPosition(eleR.x, eleR.y); 8510 pain = pain.parent; 8511 } 8512 8513 void populateMouseEventBase(MouseEventBase event) { 8514 event.button = ev.button; 8515 event.buttonLinear = ev.buttonLinear; 8516 event.state = ev.modifierState; 8517 event.clientX = eleR.x; 8518 event.clientY = eleR.y; 8519 8520 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8521 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8522 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8523 } 8524 8525 if(ev.type == MouseEventType.buttonPressed) { 8526 { 8527 auto event = new MouseDownEvent(captureEle); 8528 populateMouseEventBase(event); 8529 event.dispatch(); 8530 } 8531 8532 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8533 auto event = new DoubleClickEvent(captureEle); 8534 populateMouseEventBase(event); 8535 event.dispatch(); 8536 lastWasDoubleClick = ev.doubleClick; 8537 } else { 8538 lastWasDoubleClick = false; 8539 } 8540 8541 mouseLastDownOn = ele; 8542 } else if(ev.type == MouseEventType.buttonReleased) { 8543 { 8544 auto event = new MouseUpEvent(captureEle); 8545 populateMouseEventBase(event); 8546 event.dispatch(); 8547 } 8548 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8549 auto event = new ClickEvent(captureEle); 8550 populateMouseEventBase(event); 8551 event.dispatch(); 8552 } 8553 } else if(ev.type == MouseEventType.motion) { 8554 // motion 8555 { 8556 auto event = new MouseMoveEvent(captureEle); 8557 populateMouseEventBase(event); // fills in button which is meaningless but meh 8558 event.dispatch(); 8559 } 8560 8561 if(mouseLastOver !is ele) { 8562 if(ele !is null) { 8563 if(!isAParentOf(ele, mouseLastOver)) { 8564 ele.setDynamicState(DynamicState.hover, true); 8565 auto event = new MouseEnterEvent(ele); 8566 event.relatedTarget = mouseLastOver; 8567 event.sendDirectly(); 8568 8569 ele.useStyleProperties((scope Widget.Style s) { 8570 ele.parentWindow.win.cursor = s.cursor; 8571 }); 8572 } 8573 } 8574 8575 if(mouseLastOver !is null) { 8576 if(!isAParentOf(mouseLastOver, ele)) { 8577 mouseLastOver.setDynamicState(DynamicState.hover, false); 8578 auto event = new MouseLeaveEvent(mouseLastOver); 8579 event.relatedTarget = ele; 8580 event.sendDirectly(); 8581 } 8582 } 8583 8584 if(ele !is null) { 8585 auto event = new MouseOverEvent(ele); 8586 event.relatedTarget = mouseLastOver; 8587 event.dispatch(); 8588 } 8589 8590 if(mouseLastOver !is null) { 8591 auto event = new MouseOutEvent(mouseLastOver); 8592 event.relatedTarget = ele; 8593 event.dispatch(); 8594 } 8595 8596 mouseLastOver = ele; 8597 } 8598 } 8599 8600 return true; // FIXME: the event default prevented? 8601 } 8602 8603 /++ 8604 Shows the window and runs the application event loop. 8605 8606 Blocks until this window is closed. 8607 8608 Bugs: 8609 8610 $(PITFALL 8611 You should always have one event loop live for your application. 8612 If you make two windows in sequence, the second call to loop (or 8613 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8614 might fail: 8615 8616 --- 8617 // don't do this! 8618 auto window = new Window(); 8619 window.loop(); 8620 8621 // or new Window or new MainWindow, all the same 8622 auto window2 = new SimpleWindow(); 8623 window2.eventLoop(0); // problematic! might crash 8624 --- 8625 8626 simpledisplay's current implementation assumes that final cleanup is 8627 done when the event loop refcount reaches zero. So after the first 8628 eventLoop returns, when there isn't already another one active, it assumes 8629 the program will exit soon and cleans up. 8630 8631 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8632 it eventually, but in the mean time, there's an easy solution: 8633 8634 --- 8635 // do this 8636 EventLoop mainEventLoop = EventLoop.get; // just add this line 8637 8638 auto window = new Window(); 8639 window.loop(); 8640 8641 // or any other type of Window etc. 8642 auto window2 = new Window(); 8643 window2.loop(); // perfectly fine since mainEventLoop still alive 8644 --- 8645 8646 By adding a top-level reference to the event loop, it ensures the final cleanup 8647 is not performed until it goes out of scope too, letting the individual window loops 8648 work without trouble despite the bug. 8649 ) 8650 8651 History: 8652 The [BlockingMode] parameter was added on December 8, 2021. 8653 The default behavior is to block until the application quits 8654 (so all windows have been closed), unless another minigui or 8655 simpledisplay event loop is already running, in which case it 8656 will block until this window closes specifically. 8657 +/ 8658 @scriptable 8659 void loop(BlockingMode bm = BlockingMode.automatic) { 8660 if(win.closed) 8661 return; // otherwise show will throw 8662 show(); 8663 win.eventLoopWithBlockingMode(bm, 0); 8664 } 8665 8666 private bool firstShow = true; 8667 8668 @scriptable 8669 override void show() { 8670 bool rd = false; 8671 if(firstShow) { 8672 firstShow = false; 8673 queueRecomputeChildLayout(); 8674 auto f = getFirstFocusable(this); // FIXME: autofocus? 8675 if(f) 8676 f.focus(); 8677 redraw(); 8678 } 8679 win.show(); 8680 super.show(); 8681 } 8682 @scriptable 8683 override void hide() { 8684 win.hide(); 8685 super.hide(); 8686 } 8687 8688 static Widget getFirstFocusable(Widget start) { 8689 if(start is null) 8690 return null; 8691 8692 foreach(widget; &start.focusableWidgets) { 8693 return widget; 8694 } 8695 8696 return null; 8697 } 8698 8699 static Widget getLastFocusable(Widget start) { 8700 if(start is null) 8701 return null; 8702 8703 Widget last; 8704 foreach(widget; &start.focusableWidgets) { 8705 last = widget; 8706 } 8707 8708 return last; 8709 } 8710 8711 8712 mixin Emits!ClosingEvent; 8713 mixin Emits!ClosedEvent; 8714 } 8715 8716 /++ 8717 History: 8718 Added January 12, 2022 8719 +/ 8720 class DpiChangedEvent : Event { 8721 enum EventString = "dpichanged"; 8722 8723 this(Widget target) { 8724 super(EventString, target); 8725 } 8726 } 8727 8728 debug private class DevToolWindow : Window { 8729 Window p; 8730 8731 TextEdit parentList; 8732 TextEdit logWindow; 8733 TextLabel clickX, clickY; 8734 8735 this(Window p) { 8736 this.p = p; 8737 super(400, 300, "Developer Toolbox"); 8738 8739 logWindow = new TextEdit(this); 8740 parentList = new TextEdit(this); 8741 8742 auto hl = new HorizontalLayout(this); 8743 clickX = new TextLabel("", TextAlignment.Right, hl); 8744 clickY = new TextLabel("", TextAlignment.Right, hl); 8745 8746 parentListeners ~= p.addEventListener("*", (Event ev) { 8747 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8748 }); 8749 8750 parentListeners ~= p.addEventListener((ClickEvent ev) { 8751 auto s = ev.srcElement; 8752 8753 string list; 8754 8755 void addInfo(Widget s) { 8756 list ~= s.toString(); 8757 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8758 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8759 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8760 list ~= "\n\theight: " ~ toInternal!string(s.height); 8761 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 8762 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 8763 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 8764 list ~= "\n\twidth: " ~ toInternal!string(s.width); 8765 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8766 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8767 } 8768 8769 addInfo(s); 8770 8771 s = s.parent; 8772 while(s) { 8773 list ~= "\n"; 8774 addInfo(s); 8775 s = s.parent; 8776 } 8777 parentList.content = list; 8778 8779 clickX.label = toInternal!string(ev.clientX); 8780 clickY.label = toInternal!string(ev.clientY); 8781 }); 8782 } 8783 8784 EventListener[] parentListeners; 8785 8786 override void close() { 8787 assert(p !is null); 8788 foreach(p; parentListeners) 8789 p.disconnect(); 8790 parentListeners = null; 8791 p.devTools = null; 8792 p = null; 8793 super.close(); 8794 } 8795 8796 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8797 if(ev.key == Key.F12) { 8798 this.close(); 8799 if(p) 8800 p.devTools = null; 8801 } else { 8802 super.defaultEventHandler_keydown(ev); 8803 } 8804 } 8805 8806 void log(T...)(T t) { 8807 string str; 8808 import std.conv; 8809 foreach(i; t) 8810 str ~= to!string(i); 8811 str ~= "\n"; 8812 logWindow.addText(str); 8813 8814 //version(custom_widgets) 8815 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8816 } 8817 } 8818 8819 /++ 8820 A dialog is a transient window that intends to get information from 8821 the user before being dismissed. 8822 +/ 8823 abstract class Dialog : Window { 8824 /// 8825 this(int width, int height, string title = null) { 8826 super(width, height, title); 8827 } 8828 8829 /// 8830 abstract void OK(); 8831 8832 /// 8833 void Cancel() { 8834 this.close(); 8835 } 8836 } 8837 8838 /++ 8839 A custom widget similar to the HTML5 <details> tag. 8840 +/ 8841 version(none) 8842 class DetailsView : Widget { 8843 8844 } 8845 8846 // FIXME: maybe i should expose the other list views Windows offers too 8847 8848 /++ 8849 A TableView is a widget made to display a table of data strings. 8850 8851 8852 Future_Directions: 8853 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8854 8855 I will add a selection changed event at some point, as well as item clicked events. 8856 History: 8857 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8858 See_Also: 8859 [ListWidget] which displays a list of strings without additional columns. 8860 +/ 8861 class TableView : Widget { 8862 /++ 8863 8864 +/ 8865 this(Widget parent) { 8866 super(parent); 8867 8868 version(win32_widgets) { 8869 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8870 } else version(custom_widgets) { 8871 auto smw = new ScrollMessageWidget(this); 8872 smw.addDefaultKeyboardListeners(); 8873 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8874 tvwi = new TableViewWidgetInner(this, smw); 8875 } 8876 } 8877 8878 // FIXME: auto-size columns on double click of header thing like in Windows 8879 // it need only make the currently displayed things fit well. 8880 8881 8882 private ColumnInfo[] columns; 8883 private int itemCount; 8884 8885 version(custom_widgets) private { 8886 TableViewWidgetInner tvwi; 8887 } 8888 8889 /// Passed to [setColumnInfo] 8890 static struct ColumnInfo { 8891 const(char)[] name; /// the name displayed in the header 8892 /++ 8893 The default width, in pixels. As a special case, you can set this to -1 8894 if you want the system to try to automatically size the width to fit visible 8895 content. If it can't, it will try to pick a sensible default size. 8896 8897 Any other negative value is not allowed and may lead to unpredictable results. 8898 8899 History: 8900 The -1 behavior was specified on December 3, 2021. It actually worked before 8901 anyway on Win32 but now it is a formal feature with partial Linux support. 8902 8903 Bugs: 8904 It doesn't actually attempt to calculate a best-fit width on Linux as of 8905 December 3, 2021. I do plan to fix this in the future, but Windows is the 8906 priority right now. At least it doesn't break things when you use it now. 8907 +/ 8908 int width; 8909 8910 /++ 8911 Alignment of the text in the cell. Applies to the header as well as all data in this 8912 column. 8913 8914 Bugs: 8915 On Windows, the first column ignores this member and is always left aligned. 8916 You can work around this by inserting a dummy first column with width = 0 8917 then putting your actual data in the second column, which does respect the 8918 alignment. 8919 8920 This is a quirk of the operating system's implementation going back a very 8921 long time and is unlikely to ever be fixed. 8922 +/ 8923 TextAlignment alignment; 8924 8925 /++ 8926 After all the pixel widths have been assigned, any left over 8927 space is divided up among all columns and distributed to according 8928 to the widthPercent field. 8929 8930 8931 For example, if you have two fields, both with width 50 and one with 8932 widthPercent of 25 and the other with widthPercent of 75, and the 8933 container is 200 pixels wide, first both get their width of 50. 8934 then the 100 remaining pixels are split up, so the one gets a total 8935 of 75 pixels and the other gets a total of 125. 8936 8937 This is automatically applied as the window is resized. 8938 8939 If there is not enough space - that is, when a horizontal scrollbar 8940 needs to appear - there are 0 pixels divided up, and thus everyone 8941 gets 0. This can cause a column to shrink out of proportion when 8942 passing the scroll threshold. 8943 8944 It is important to still set a fixed width (that is, to populate the 8945 `width` field) even if you use the percents because that will be the 8946 default minimum in the event of a scroll bar appearing. 8947 8948 The percents total in the column can never exceed 100 or be less than 0. 8949 Doing this will trigger an assert error. 8950 8951 Implementation note: 8952 8953 Please note that percentages are only recalculated 1) upon original 8954 construction and 2) upon resizing the control. If the user adjusts the 8955 width of a column, the percentage items will not be updated. 8956 8957 On the other hand, if the user adjusts the width of a percentage column 8958 then resizes the window, it is recalculated, meaning their hand adjustment 8959 is discarded. This specific behavior may change in the future as it is 8960 arguably a bug, but I'm not certain yet. 8961 8962 History: 8963 Added November 10, 2021 (dub v10.4) 8964 +/ 8965 int widthPercent; 8966 8967 8968 private int calculatedWidth; 8969 } 8970 /++ 8971 Sets the number of columns along with information about the headers. 8972 8973 Please note: on Windows, the first column ignores your alignment preference 8974 and is always left aligned. 8975 +/ 8976 void setColumnInfo(ColumnInfo[] columns...) { 8977 8978 foreach(ref c; columns) { 8979 c.name = c.name.idup; 8980 } 8981 this.columns = columns.dup; 8982 8983 updateCalculatedWidth(false); 8984 8985 version(custom_widgets) { 8986 tvwi.header.updateHeaders(); 8987 tvwi.updateScrolls(); 8988 } else version(win32_widgets) 8989 foreach(i, column; this.columns) { 8990 LVCOLUMN lvColumn; 8991 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8992 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8993 8994 auto bfr = WCharzBuffer(column.name); 8995 lvColumn.pszText = bfr.ptr; 8996 8997 if(column.alignment & TextAlignment.Center) 8998 lvColumn.fmt = LVCFMT_CENTER; 8999 else if(column.alignment & TextAlignment.Right) 9000 lvColumn.fmt = LVCFMT_RIGHT; 9001 else 9002 lvColumn.fmt = LVCFMT_LEFT; 9003 9004 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 9005 throw new WindowsApiException("Insert Column Fail", GetLastError()); 9006 } 9007 } 9008 9009 private int getActualSetSize(size_t i, bool askWindows) { 9010 version(win32_widgets) 9011 if(askWindows) 9012 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 9013 auto w = columns[i].width; 9014 if(w == -1) 9015 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 9016 return w; 9017 } 9018 9019 private void updateCalculatedWidth(bool informWindows) { 9020 int padding; 9021 version(win32_widgets) 9022 padding = 4; 9023 int remaining = this.width; 9024 foreach(i, column; columns) 9025 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9026 remaining -= padding; 9027 if(remaining < 0) 9028 remaining = 0; 9029 9030 int percentTotal; 9031 foreach(i, ref column; columns) { 9032 percentTotal += column.widthPercent; 9033 9034 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9035 9036 column.calculatedWidth = c; 9037 9038 version(win32_widgets) 9039 if(informWindows) 9040 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9041 } 9042 9043 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9044 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)."); 9045 9046 9047 } 9048 9049 override void registerMovement() { 9050 super.registerMovement(); 9051 9052 updateCalculatedWidth(true); 9053 } 9054 9055 /++ 9056 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. 9057 +/ 9058 void setItemCount(int count) { 9059 this.itemCount = count; 9060 version(custom_widgets) { 9061 tvwi.updateScrolls(); 9062 redraw(); 9063 } else version(win32_widgets) { 9064 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9065 } 9066 } 9067 9068 /++ 9069 Clears all items; 9070 +/ 9071 void clear() { 9072 this.itemCount = 0; 9073 this.columns = null; 9074 version(custom_widgets) { 9075 tvwi.header.updateHeaders(); 9076 tvwi.updateScrolls(); 9077 redraw(); 9078 } else version(win32_widgets) { 9079 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9080 } 9081 } 9082 9083 /+ 9084 version(win32_widgets) 9085 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9086 auto itemId = dis.itemID; 9087 auto hdc = dis.hDC; 9088 auto rect = dis.rcItem; 9089 switch(dis.itemAction) { 9090 case ODA_DRAWENTIRE: 9091 9092 // FIXME: do other items 9093 // FIXME: do the focus rectangle i guess 9094 // FIXME: alignment 9095 // FIXME: column width 9096 // FIXME: padding left 9097 // FIXME: check dpi scaling 9098 // FIXME: don't owner draw unless it is necessary. 9099 9100 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9101 RECT itemRect; 9102 itemRect.top = 1; // subitem idx, 1-based 9103 itemRect.left = LVIR_BOUNDS; 9104 9105 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9106 itemRect.left += padding; 9107 9108 getData(itemId, 0, (in char[] data) { 9109 auto wdata = WCharzBuffer(data); 9110 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9111 9112 }); 9113 goto case; 9114 case ODA_FOCUS: 9115 if(dis.itemState & ODS_FOCUS) 9116 DrawFocusRect(hdc, &rect); 9117 break; 9118 case ODA_SELECT: 9119 // itemState & ODS_SELECTED 9120 break; 9121 default: 9122 } 9123 return 1; 9124 } 9125 +/ 9126 9127 version(win32_widgets) { 9128 CellStyle last; 9129 COLORREF defaultColor; 9130 COLORREF defaultBackground; 9131 } 9132 9133 version(win32_widgets) 9134 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9135 switch(code) { 9136 case NM_CUSTOMDRAW: 9137 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9138 switch(s.nmcd.dwDrawStage) { 9139 case CDDS_PREPAINT: 9140 if(getCellStyle is null) 9141 return 0; 9142 9143 mustReturn = true; 9144 return CDRF_NOTIFYITEMDRAW; 9145 case CDDS_ITEMPREPAINT: 9146 mustReturn = true; 9147 return CDRF_NOTIFYSUBITEMDRAW; 9148 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9149 mustReturn = true; 9150 9151 if(getCellStyle is null) // this SHOULD never happen... 9152 return 0; 9153 9154 if(s.iSubItem == 0) { 9155 // Windows resets it per row so we'll use item 0 as a chance 9156 // to capture these for later 9157 defaultColor = s.clrText; 9158 defaultBackground = s.clrTextBk; 9159 } 9160 9161 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9162 // if no special style and no reset needed... 9163 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9164 return 0; // allow default processing to continue 9165 9166 last = style; 9167 9168 // might still need to reset or use the preference. 9169 9170 if(style.flags & CellStyle.Flags.textColorSet) 9171 s.clrText = style.textColor.asWindowsColorRef; 9172 else 9173 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9174 if(style.flags & CellStyle.Flags.backgroundColorSet) 9175 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9176 else 9177 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9178 9179 return CDRF_NEWFONT; 9180 default: 9181 return 0; 9182 9183 } 9184 case NM_RETURN: // no need since i subclass keydown 9185 break; 9186 case LVN_COLUMNCLICK: 9187 auto info = cast(LPNMLISTVIEW) hdr; 9188 this.emit!HeaderClickedEvent(info.iSubItem); 9189 break; 9190 case NM_CLICK: 9191 case NM_DBLCLK: 9192 case NM_RCLICK: 9193 case NM_RDBLCLK: 9194 // the item/subitem is set here and that can be a useful notification 9195 // even beyond the normal click notification 9196 break; 9197 case LVN_GETDISPINFO: 9198 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9199 if(info.item.mask & LVIF_TEXT) { 9200 if(getData) { 9201 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9202 auto bfr = WCharzBuffer(dataReceived); 9203 auto len = info.item.cchTextMax; 9204 if(bfr.length < len) 9205 len = cast(typeof(len)) bfr.length; 9206 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9207 info.item.pszText[len] = 0; 9208 }); 9209 } else { 9210 info.item.pszText[0] = 0; 9211 } 9212 //info.item.iItem 9213 //if(info.item.iSubItem) 9214 } 9215 break; 9216 default: 9217 } 9218 return 0; 9219 } 9220 9221 override bool encapsulatedChildren() { 9222 return true; 9223 } 9224 9225 /++ 9226 Informs the control that content has changed. 9227 9228 History: 9229 Added November 10, 2021 (dub v10.4) 9230 +/ 9231 void update() { 9232 version(custom_widgets) 9233 redraw(); 9234 else { 9235 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9236 UpdateWindow(hwnd); 9237 } 9238 9239 9240 } 9241 9242 /++ 9243 Called by the system to request the text content of an individual cell. You 9244 should pass the text into the provided `sink` delegate. This function will be 9245 called for each visible cell as-needed when drawing. 9246 +/ 9247 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9248 9249 /++ 9250 Available per-cell style customization options. Use one of the constructors 9251 provided to set the values conveniently, or default construct it and set individual 9252 values yourself. Just remember to set the `flags` so your values are actually used. 9253 If the flag isn't set, the field is ignored and the system default is used instead. 9254 9255 This is returned by the [getCellStyle] delegate. 9256 9257 Examples: 9258 --- 9259 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9260 auto table = new TableView(window); 9261 // snip: you would set up columns here 9262 9263 // this is how you provide data to the table view class 9264 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9265 import std.conv; 9266 sink(to!string(my_data[row][column])); 9267 }; 9268 9269 // and this is how you customize the colors 9270 table.getCellStyle = delegate(int row, int column) { 9271 return (my_data[row][column] < 0) ? 9272 TableView.CellStyle(Color.red); // make negative numbers red 9273 : TableView.CellStyle.init; // leave the rest alone 9274 }; 9275 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9276 --- 9277 9278 History: 9279 Added November 27, 2021 (dub v10.4) 9280 +/ 9281 struct CellStyle { 9282 /// 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. 9283 this(Color textColor) { 9284 this.textColor = textColor; 9285 this.flags |= Flags.textColorSet; 9286 } 9287 /// Sets a custom text and background color. 9288 this(Color textColor, Color backgroundColor) { 9289 this.textColor = textColor; 9290 this.backgroundColor = backgroundColor; 9291 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9292 } 9293 9294 Color textColor; 9295 Color backgroundColor; 9296 int flags; /// bitmask of [Flags] 9297 /// available options to combine into [flags] 9298 enum Flags { 9299 textColorSet = 1 << 0, 9300 backgroundColorSet = 1 << 1, 9301 } 9302 } 9303 /++ 9304 Companion delegate to [getData] that allows you to custom style each 9305 cell of the table. 9306 9307 Returns: 9308 A [CellStyle] structure that describes the desired style for the 9309 given cell. `return CellStyle.init` if you want the default style. 9310 9311 History: 9312 Added November 27, 2021 (dub v10.4) 9313 +/ 9314 CellStyle delegate(int row, int column) getCellStyle; 9315 9316 // i want to be able to do things like draw little colored things to show red for negative numbers 9317 // or background color indicators or even in-cell charts 9318 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9319 9320 /++ 9321 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9322 +/ 9323 mixin Emits!HeaderClickedEvent; 9324 } 9325 9326 /++ 9327 This is emitted by the [TableView] when a user clicks on a column header. 9328 9329 Its member `columnIndex` has the zero-based index of the column that was clicked. 9330 9331 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9332 9333 History: 9334 Added November 27, 2021 (dub v10.4) 9335 +/ 9336 class HeaderClickedEvent : Event { 9337 enum EventString = "HeaderClicked"; 9338 this(Widget target, int columnIndex) { 9339 this.columnIndex = columnIndex; 9340 super(EventString, target); 9341 } 9342 9343 /// The index of the column 9344 int columnIndex; 9345 9346 /// 9347 override @property int intValue() { 9348 return columnIndex; 9349 } 9350 } 9351 9352 version(custom_widgets) 9353 private class TableViewWidgetInner : Widget { 9354 9355 // wrap this thing in a ScrollMessageWidget 9356 9357 TableView tvw; 9358 ScrollMessageWidget smw; 9359 HeaderWidget header; 9360 9361 this(TableView tvw, ScrollMessageWidget smw) { 9362 this.tvw = tvw; 9363 this.smw = smw; 9364 super(smw); 9365 9366 this.tabStop = true; 9367 9368 header = new HeaderWidget(this, smw.getHeader()); 9369 9370 smw.addEventListener("scroll", () { 9371 this.redraw(); 9372 header.redraw(); 9373 }); 9374 9375 9376 // I need headers outside the scroll area but rendered on the same line as the up arrow 9377 // FIXME: add a fixed header to the SMW 9378 } 9379 9380 enum padding = 3; 9381 9382 void updateScrolls() { 9383 int w; 9384 foreach(idx, column; tvw.columns) { 9385 if(column.width == 0) continue; 9386 w += tvw.getActualSetSize(idx, false);// + padding; 9387 } 9388 smw.setTotalArea(w, tvw.itemCount); 9389 columnsWidth = w; 9390 } 9391 9392 private int columnsWidth; 9393 9394 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9395 9396 override void registerMovement() { 9397 super.registerMovement(); 9398 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9399 smw.setViewableArea(this.width, this.height / lh); 9400 } 9401 9402 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9403 int x; 9404 int y; 9405 9406 int row = smw.position.y; 9407 9408 foreach(lol; 0 .. this.height / lh) { 9409 if(row >= tvw.itemCount) 9410 break; 9411 x = 0; 9412 foreach(columnNumber, column; tvw.columns) { 9413 auto x2 = x + column.calculatedWidth; 9414 auto smwx = smw.position.x; 9415 9416 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9417 auto startX = x; 9418 auto endX = x + column.calculatedWidth; 9419 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9420 case TextAlignment.Left: startX += padding; break; 9421 case TextAlignment.Center: startX += padding; endX -= padding; break; 9422 case TextAlignment.Right: endX -= padding; break; 9423 default: /* broken */ break; 9424 } 9425 if(column.width != 0) // no point drawing an invisible column 9426 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9427 // auto clip = painter.setClipRectangle( 9428 9429 void dotext(WidgetPainter painter) { 9430 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9431 } 9432 9433 if(tvw.getCellStyle !is null) { 9434 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9435 9436 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9437 auto tempPainter = painter; 9438 tempPainter.fillColor = style.backgroundColor; 9439 tempPainter.outlineColor = style.backgroundColor; 9440 9441 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9442 Point(endX - smw.position.x, y + lh)); 9443 } 9444 auto tempPainter = painter; 9445 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9446 tempPainter.outlineColor = style.textColor; 9447 9448 dotext(tempPainter); 9449 } else { 9450 dotext(painter); 9451 } 9452 }); 9453 } 9454 9455 x += column.calculatedWidth; 9456 } 9457 row++; 9458 y += lh; 9459 } 9460 return bounds; 9461 } 9462 9463 static class Style : Widget.Style { 9464 override WidgetBackground background() { 9465 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9466 } 9467 } 9468 mixin OverrideStyle!Style; 9469 9470 private static class HeaderWidget : Widget { 9471 this(TableViewWidgetInner tvw, Widget parent) { 9472 super(parent); 9473 this.tvw = tvw; 9474 9475 this.remainder = new Button("", this); 9476 9477 this.addEventListener((scope ClickEvent ev) { 9478 int header = -1; 9479 foreach(idx, child; this.children[1 .. $]) { 9480 if(child is ev.target) { 9481 header = cast(int) idx; 9482 break; 9483 } 9484 } 9485 9486 if(header != -1) { 9487 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9488 hce.dispatch(); 9489 } 9490 9491 }); 9492 } 9493 9494 void updateHeaders() { 9495 foreach(child; children[1 .. $]) 9496 child.removeWidget(); 9497 9498 foreach(column; tvw.tvw.columns) { 9499 // the cast is ok because I dup it above, just the type is never changed. 9500 // all this is private so it should never get messed up. 9501 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9502 } 9503 } 9504 9505 Button remainder; 9506 TableViewWidgetInner tvw; 9507 9508 override void recomputeChildLayout() { 9509 registerMovement(); 9510 int pos; 9511 foreach(idx, child; children[1 .. $]) { 9512 if(idx >= tvw.tvw.columns.length) 9513 continue; 9514 child.x = pos; 9515 child.y = 0; 9516 child.width = tvw.tvw.columns[idx].calculatedWidth; 9517 child.height = scaleWithDpi(16);// this.height; 9518 pos += child.width; 9519 9520 child.recomputeChildLayout(); 9521 } 9522 9523 if(remainder is null) 9524 return; 9525 9526 remainder.x = pos; 9527 remainder.y = 0; 9528 if(pos < this.width) 9529 remainder.width = this.width - pos;// + 4; 9530 else 9531 remainder.width = 0; 9532 remainder.height = scaleWithDpi(16); 9533 9534 remainder.recomputeChildLayout(); 9535 } 9536 9537 // for the scrollable children mixin 9538 Point scrollOrigin() { 9539 return Point(tvw.smw.position.x, 0); 9540 } 9541 void paintFrameAndBackground(WidgetPainter painter) { } 9542 9543 mixin ScrollableChildren; 9544 } 9545 } 9546 9547 /+ 9548 9549 // given struct / array / number / string / etc, make it viewable and editable 9550 class DataViewerWidget : Widget { 9551 9552 } 9553 +/ 9554 9555 /++ 9556 A line edit box with an associated label. 9557 9558 History: 9559 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9560 9561 ``` 9562 Old: ________ 9563 9564 New: 9565 ____________ 9566 ``` 9567 9568 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9569 9570 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9571 horizontal label but left aligned. You may also consider a [GridLayout]. 9572 +/ 9573 alias LabeledLineEdit = Labeled!LineEdit; 9574 9575 private int widthThatWouldFitChildLabels(Widget w) { 9576 if(w is null) 9577 return 0; 9578 9579 int max; 9580 9581 if(auto label = cast(TextLabel) w) { 9582 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 9583 } else { 9584 foreach(child; w.children) { 9585 max = mymax(max, widthThatWouldFitChildLabels(child)); 9586 } 9587 } 9588 9589 return max; 9590 } 9591 9592 /++ 9593 History: 9594 Added May 19, 2021 9595 +/ 9596 class Labeled(T) : Widget { 9597 /// 9598 this(string label, Widget parent) { 9599 super(parent); 9600 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9601 } 9602 9603 /++ 9604 History: 9605 The alignment parameter was added May 17, 2021 9606 +/ 9607 this(string label, TextAlignment alignment, Widget parent) { 9608 super(parent); 9609 initialize!HorizontalLayout(label, alignment, parent); 9610 } 9611 9612 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9613 tabStop = false; 9614 horizontal = is(L == HorizontalLayout); 9615 auto hl = new L(this); 9616 if(horizontal) { 9617 static class SpecialTextLabel : TextLabel { 9618 Widget outerParent; 9619 9620 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 9621 this.outerParent = outerParent; 9622 super(label, alignment, parent); 9623 } 9624 9625 override int flexBasisWidth() { 9626 return widthThatWouldFitChildLabels(outerParent); 9627 } 9628 /+ 9629 override int widthShrinkiness() { return 0; } 9630 override int widthStretchiness() { return 1; } 9631 +/ 9632 9633 override int paddingRight() { return 6; } 9634 override int paddingLeft() { return 9; } 9635 9636 override int paddingTop() { return 3; } 9637 } 9638 this.label = new SpecialTextLabel(label, alignment, parent, hl); 9639 } else 9640 this.label = new TextLabel(label, alignment, hl); 9641 this.lineEdit = new T(hl); 9642 9643 this.label.labelFor = this.lineEdit; 9644 } 9645 9646 private bool horizontal; 9647 9648 TextLabel label; /// 9649 T lineEdit; /// 9650 9651 override int flexBasisWidth() { return 250; } 9652 override int widthShrinkiness() { return 1; } 9653 9654 override int minHeight() { 9655 return this.children[0].minHeight; 9656 } 9657 override int maxHeight() { return minHeight(); } 9658 override int marginTop() { return 4; } 9659 override int marginBottom() { return 4; } 9660 9661 // FIXME: i should prolly call it value as well as content tbh 9662 9663 /// 9664 @property string content() { 9665 return lineEdit.content; 9666 } 9667 /// 9668 @property void content(string c) { 9669 return lineEdit.content(c); 9670 } 9671 9672 /// 9673 void selectAll() { 9674 lineEdit.selectAll(); 9675 } 9676 9677 override void focus() { 9678 lineEdit.focus(); 9679 } 9680 } 9681 9682 /++ 9683 A labeled password edit. 9684 9685 History: 9686 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9687 9688 The default parameters for the constructors were also removed on May 19, 2021 9689 +/ 9690 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9691 9692 private string toMenuLabel(string s) { 9693 string n; 9694 n.reserve(s.length); 9695 foreach(c; s) 9696 if(c == '_') 9697 n ~= ' '; 9698 else 9699 n ~= c; 9700 return n; 9701 } 9702 9703 private void autoExceptionHandler(Exception e) { 9704 messageBox(e.msg); 9705 } 9706 9707 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9708 static if(is(T : void delegate())) { 9709 return () { 9710 try 9711 t(); 9712 catch(Exception e) 9713 autoExceptionHandler(e); 9714 }; 9715 } else static if(is(typeof(fn) Params == __parameters)) { 9716 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9717 return () { 9718 void onOK(string s) { 9719 member = s; 9720 try 9721 t(Params[0](s)); 9722 catch(Exception e) 9723 autoExceptionHandler(e); 9724 } 9725 9726 if( 9727 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9728 || type == FileDialogType.Save) 9729 { 9730 getSaveFileName(&onOK, member, filters, null); 9731 } else 9732 getOpenFileName(&onOK, member, filters, null); 9733 }; 9734 } else { 9735 struct S { 9736 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9737 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9738 } else mixin(q{ 9739 static foreach(idx, ignore; Params) { 9740 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9741 } 9742 }); 9743 } 9744 return () { 9745 dialog((S s) { 9746 try { 9747 static if(is(typeof(t) Ret == return)) { 9748 static if(is(Ret == void)) { 9749 t(s.tupleof); 9750 } else { 9751 auto ret = t(s.tupleof); 9752 import std.conv; 9753 messageBox(to!string(ret), "Returned Value"); 9754 } 9755 } 9756 } catch(Exception e) 9757 autoExceptionHandler(e); 9758 }, null, __traits(identifier, fn)); 9759 }; 9760 } 9761 } 9762 } 9763 9764 private template hasAnyRelevantAnnotations(a...) { 9765 bool helper() { 9766 bool any; 9767 foreach(attr; a) { 9768 static if(is(typeof(attr) == .menu)) 9769 any = true; 9770 else static if(is(typeof(attr) == .toolbar)) 9771 any = true; 9772 else static if(is(attr == .separator)) 9773 any = true; 9774 else static if(is(typeof(attr) == .accelerator)) 9775 any = true; 9776 else static if(is(typeof(attr) == .hotkey)) 9777 any = true; 9778 else static if(is(typeof(attr) == .icon)) 9779 any = true; 9780 else static if(is(typeof(attr) == .label)) 9781 any = true; 9782 else static if(is(typeof(attr) == .tip)) 9783 any = true; 9784 } 9785 return any; 9786 } 9787 9788 enum bool hasAnyRelevantAnnotations = helper(); 9789 } 9790 9791 /++ 9792 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. 9793 +/ 9794 class MainWindow : Window { 9795 /// 9796 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9797 super(initialWidth, initialHeight, title); 9798 9799 _clientArea = new ClientAreaWidget(); 9800 _clientArea.x = 0; 9801 _clientArea.y = 0; 9802 _clientArea.width = this.width; 9803 _clientArea.height = this.height; 9804 _clientArea.tabStop = false; 9805 9806 super.addChild(_clientArea); 9807 9808 statusBar = new StatusBar(this); 9809 } 9810 9811 /++ 9812 Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though). 9813 9814 --- 9815 struct Commands { 9816 @menu("File") { 9817 @toolbar("") // adds it to a generic toolbar 9818 void New() {} 9819 void Open() {} 9820 void Save() {} 9821 @separator 9822 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9823 window.close(); 9824 } 9825 } 9826 9827 @menu("Edit") { 9828 @icon(GenericIcons.Undo) 9829 void Undo() { 9830 undo(); 9831 } 9832 @separator 9833 void Cut() {} 9834 void Copy() {} 9835 void Paste() {} 9836 } 9837 9838 @menu("Help") { 9839 void About() {} 9840 } 9841 } 9842 9843 Commands commands; 9844 9845 window.setMenuAndToolbarFromAnnotatedCode(commands); 9846 --- 9847 9848 Note that you can call this function multiple times and it will add the items in order to the given items. 9849 9850 +/ 9851 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9852 setMenuAndToolbarFromAnnotatedCode_internal(t); 9853 } 9854 /// ditto 9855 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9856 setMenuAndToolbarFromAnnotatedCode_internal(t); 9857 } 9858 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9859 Action[] toolbarActions; 9860 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9861 Menu[string] mcs; 9862 9863 foreach(menu; menuBar.subMenus) { 9864 mcs[menu.label] = menu; 9865 } 9866 9867 foreach(memberName; __traits(derivedMembers, T)) { 9868 static if(memberName != "this") 9869 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9870 .menu menu; 9871 .toolbar toolbar; 9872 bool separator; 9873 .accelerator accelerator; 9874 .hotkey hotkey; 9875 .icon icon; 9876 string label; 9877 string tip; 9878 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9879 static if(is(typeof(attr) == .menu)) 9880 menu = attr; 9881 else static if(is(typeof(attr) == .toolbar)) 9882 toolbar = attr; 9883 else static if(is(attr == .separator)) 9884 separator = true; 9885 else static if(is(typeof(attr) == .accelerator)) 9886 accelerator = attr; 9887 else static if(is(typeof(attr) == .hotkey)) 9888 hotkey = attr; 9889 else static if(is(typeof(attr) == .icon)) 9890 icon = attr; 9891 else static if(is(typeof(attr) == .label)) 9892 label = attr.label; 9893 else static if(is(typeof(attr) == .tip)) 9894 tip = attr.tip; 9895 } 9896 9897 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9898 ushort correctIcon = icon.id; // FIXME 9899 if(label.length == 0) 9900 label = memberName.toMenuLabel; 9901 9902 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9903 9904 auto action = new Action(label, correctIcon, handler); 9905 9906 if(accelerator.keyString.length) { 9907 auto ke = KeyEvent.parse(accelerator.keyString); 9908 action.accelerator = ke; 9909 accelerators[ke.toStr] = handler; 9910 } 9911 9912 if(toolbar !is .toolbar.init) 9913 toolbarActions ~= action; 9914 if(menu !is .menu.init) { 9915 Menu mc; 9916 if(menu.name in mcs) { 9917 mc = mcs[menu.name]; 9918 } else { 9919 mc = new Menu(menu.name, this); 9920 menuBar.addItem(mc); 9921 mcs[menu.name] = mc; 9922 } 9923 9924 if(separator) 9925 mc.addSeparator(); 9926 mc.addItem(new MenuItem(action)); 9927 } 9928 } 9929 } 9930 } 9931 9932 this.menuBar = menuBar; 9933 9934 if(toolbarActions.length) { 9935 auto tb = new ToolBar(toolbarActions, this); 9936 } 9937 } 9938 9939 void delegate()[string] accelerators; 9940 9941 override void defaultEventHandler_keydown(KeyDownEvent event) { 9942 auto str = event.originalKeyEvent.toStr; 9943 if(auto acl = str in accelerators) 9944 (*acl)(); 9945 super.defaultEventHandler_keydown(event); 9946 } 9947 9948 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9949 super.defaultEventHandler_mouseover(event); 9950 if(this.statusBar !is null && event.target.statusTip.length) 9951 this.statusBar.parts[0].content = event.target.statusTip; 9952 else if(this.statusBar !is null && this.statusTip.length) 9953 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9954 } 9955 9956 override void addChild(Widget c, int position = int.max) { 9957 if(auto tb = cast(ToolBar) c) 9958 version(win32_widgets) 9959 super.addChild(c, 0); 9960 else version(custom_widgets) 9961 super.addChild(c, menuBar ? 1 : 0); 9962 else static assert(0); 9963 else 9964 clientArea.addChild(c, position); 9965 } 9966 9967 ToolBar _toolBar; 9968 /// 9969 ToolBar toolBar() { return _toolBar; } 9970 /// 9971 ToolBar toolBar(ToolBar t) { 9972 _toolBar = t; 9973 foreach(child; this.children) 9974 if(child is t) 9975 return t; 9976 version(win32_widgets) 9977 super.addChild(t, 0); 9978 else version(custom_widgets) 9979 super.addChild(t, menuBar ? 1 : 0); 9980 else static assert(0); 9981 return t; 9982 } 9983 9984 MenuBar _menu; 9985 /// 9986 MenuBar menuBar() { return _menu; } 9987 /// 9988 MenuBar menuBar(MenuBar m) { 9989 if(m is _menu) { 9990 version(custom_widgets) 9991 queueRecomputeChildLayout(); 9992 return m; 9993 } 9994 9995 if(_menu !is null) { 9996 // make sure it is sanely removed 9997 // FIXME 9998 } 9999 10000 _menu = m; 10001 10002 version(win32_widgets) { 10003 SetMenu(parentWindow.win.impl.hwnd, m.handle); 10004 } else version(custom_widgets) { 10005 super.addChild(m, 0); 10006 10007 // clientArea.y = menu.height; 10008 // clientArea.height = this.height - menu.height; 10009 10010 queueRecomputeChildLayout(); 10011 } else static assert(false); 10012 10013 return _menu; 10014 } 10015 private Widget _clientArea; 10016 /// 10017 @property Widget clientArea() { return _clientArea; } 10018 protected @property void clientArea(Widget wid) { 10019 _clientArea = wid; 10020 } 10021 10022 private StatusBar _statusBar; 10023 /++ 10024 Returns the window's [StatusBar]. Be warned it may be `null`. 10025 +/ 10026 @property StatusBar statusBar() { return _statusBar; } 10027 /// ditto 10028 @property void statusBar(StatusBar bar) { 10029 if(_statusBar !is null) 10030 _statusBar.removeWidget(); 10031 _statusBar = bar; 10032 if(bar !is null) 10033 super.addChild(_statusBar); 10034 } 10035 } 10036 10037 /+ 10038 This is really an implementation detail of [MainWindow] 10039 +/ 10040 private class ClientAreaWidget : Widget { 10041 this() { 10042 this.tabStop = false; 10043 super(null); 10044 //sa = new ScrollableWidget(this); 10045 } 10046 /* 10047 ScrollableWidget sa; 10048 override void addChild(Widget w, int position) { 10049 if(sa is null) 10050 super.addChild(w, position); 10051 else { 10052 sa.addChild(w, position); 10053 sa.setContentSize(this.minWidth + 1, this.minHeight); 10054 writeln(sa.contentWidth, "x", sa.contentHeight); 10055 } 10056 } 10057 */ 10058 } 10059 10060 /** 10061 Toolbars are lists of buttons (typically icons) that appear under the menu. 10062 Each button ought to correspond to a menu item, represented by [Action] objects. 10063 */ 10064 class ToolBar : Widget { 10065 version(win32_widgets) { 10066 private int idealHeight; 10067 override int minHeight() { return idealHeight; } 10068 override int maxHeight() { return idealHeight; } 10069 } else version(custom_widgets) { 10070 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10071 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10072 } else static assert(false); 10073 override int heightStretchiness() { return 0; } 10074 10075 version(win32_widgets) { 10076 HIMAGELIST imageListSmall; 10077 HIMAGELIST imageListLarge; 10078 } 10079 10080 this(Widget parent) { 10081 this(null, parent); 10082 } 10083 10084 version(win32_widgets) 10085 void changeIconSize(bool useLarge) { 10086 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10087 10088 /+ 10089 SIZE size; 10090 import core.sys.windows.commctrl; 10091 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10092 idealHeight = size.cy + 4; // the plus 4 is a hack 10093 +/ 10094 10095 idealHeight = useLarge ? 34 : 26; 10096 10097 if(parent) { 10098 parent.queueRecomputeChildLayout(); 10099 parent.redraw(); 10100 } 10101 10102 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10103 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10104 } 10105 10106 /// 10107 this(Action[] actions, Widget parent) { 10108 super(parent); 10109 10110 tabStop = false; 10111 10112 version(win32_widgets) { 10113 // so i like how the flat thing looks on windows, but not on wine 10114 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10115 // leave it commented 10116 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 10117 10118 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 10119 10120 imageListSmall = ImageList_Create( 10121 // width, height 10122 16, 16, 10123 ILC_COLOR16 | ILC_MASK, 10124 16 /*numberOfButtons*/, 0); 10125 10126 imageListLarge = ImageList_Create( 10127 // width, height 10128 24, 24, 10129 ILC_COLOR16 | ILC_MASK, 10130 16 /*numberOfButtons*/, 0); 10131 10132 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 10133 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10134 10135 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10136 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10137 10138 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10139 10140 TBBUTTON[] buttons; 10141 10142 // FIXME: I_IMAGENONE is if here is no icon 10143 foreach(action; actions) 10144 buttons ~= TBBUTTON( 10145 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10146 action.id, 10147 TBSTATE_ENABLED, // state 10148 0, // style 10149 0, // reserved array, just zero it out 10150 0, // dwData 10151 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10152 ); 10153 10154 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10155 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10156 10157 /* 10158 RECT rect; 10159 GetWindowRect(hwnd, &rect); 10160 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 10161 */ 10162 10163 dpiChanged(); // to load the things calling changeIconSize the first time 10164 10165 assert(idealHeight); 10166 } else version(custom_widgets) { 10167 foreach(action; actions) 10168 new ToolButton(action, this); 10169 } else static assert(false); 10170 } 10171 10172 override void recomputeChildLayout() { 10173 .recomputeChildLayout!"width"(this); 10174 } 10175 10176 10177 version(win32_widgets) 10178 override protected void dpiChanged() { 10179 auto sz = scaleWithDpi(16); 10180 if(sz >= 20) 10181 changeIconSize(true); 10182 else 10183 changeIconSize(false); 10184 } 10185 } 10186 10187 enum toolbarIconSize = 24; 10188 10189 /// 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. 10190 class ToolButton : Button { 10191 /// 10192 this(string label, Widget parent) { 10193 super(label, parent); 10194 tabStop = false; 10195 } 10196 /// 10197 this(Action action, Widget parent) { 10198 super(action.label, parent); 10199 tabStop = false; 10200 this.action = action; 10201 } 10202 10203 version(custom_widgets) 10204 override void defaultEventHandler_click(ClickEvent event) { 10205 foreach(handler; action.triggered) 10206 handler(); 10207 } 10208 10209 Action action; 10210 10211 override int maxWidth() { return toolbarIconSize; } 10212 override int minWidth() { return toolbarIconSize; } 10213 override int maxHeight() { return toolbarIconSize; } 10214 override int minHeight() { return toolbarIconSize; } 10215 10216 version(custom_widgets) 10217 override void paint(WidgetPainter painter) { 10218 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10219 painter.outlineColor = Color.black; 10220 10221 // I want to get from 16 to 24. that's * 3 / 2 10222 static assert(toolbarIconSize >= 16); 10223 enum multiplier = toolbarIconSize / 8; 10224 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10225 switch(action.iconId) { 10226 case GenericIcons.New: 10227 painter.fillColor = Color.white; 10228 painter.drawPolygon( 10229 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10230 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10231 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10232 ); 10233 break; 10234 case GenericIcons.Save: 10235 painter.fillColor = Color.white; 10236 painter.outlineColor = Color.black; 10237 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10238 10239 // the label 10240 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10241 10242 // the slider 10243 painter.fillColor = Color.black; 10244 painter.outlineColor = Color.black; 10245 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10246 10247 painter.fillColor = Color.white; 10248 painter.outlineColor = Color.white; 10249 // the disc window 10250 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10251 break; 10252 case GenericIcons.Open: 10253 painter.fillColor = Color.white; 10254 painter.drawPolygon( 10255 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10256 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10257 painter.drawPolygon( 10258 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10259 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10260 Point(2, 6) * multiplier / divisor); 10261 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10262 break; 10263 case GenericIcons.Copy: 10264 painter.fillColor = Color.white; 10265 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10266 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10267 break; 10268 case GenericIcons.Cut: 10269 painter.fillColor = Color.transparent; 10270 painter.outlineColor = getComputedStyle.foregroundColor(); 10271 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10272 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10273 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10274 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10275 break; 10276 case GenericIcons.Paste: 10277 painter.fillColor = Color.white; 10278 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10279 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10280 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10281 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10282 painter.fillColor = Color.black; 10283 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10284 break; 10285 case GenericIcons.Help: 10286 painter.outlineColor = getComputedStyle.foregroundColor(); 10287 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10288 break; 10289 case GenericIcons.Undo: 10290 painter.fillColor = Color.transparent; 10291 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10292 painter.outlineColor = Color.black; 10293 painter.fillColor = Color.black; 10294 painter.drawPolygon( 10295 Point(4, 4) * multiplier / divisor, 10296 Point(8, 2) * multiplier / divisor, 10297 Point(8, 6) * multiplier / divisor, 10298 Point(4, 4) * multiplier / divisor, 10299 ); 10300 break; 10301 case GenericIcons.Redo: 10302 painter.fillColor = Color.transparent; 10303 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10304 painter.outlineColor = Color.black; 10305 painter.fillColor = Color.black; 10306 painter.drawPolygon( 10307 Point(10, 4) * multiplier / divisor, 10308 Point(6, 2) * multiplier / divisor, 10309 Point(6, 6) * multiplier / divisor, 10310 Point(10, 4) * multiplier / divisor, 10311 ); 10312 break; 10313 default: 10314 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10315 } 10316 return bounds; 10317 }); 10318 } 10319 10320 } 10321 10322 10323 /++ 10324 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 10325 +/ 10326 class MenuBar : Widget { 10327 MenuItem[] items; 10328 Menu[] subMenus; 10329 10330 version(win32_widgets) { 10331 HMENU handle; 10332 /// 10333 this(Widget parent = null) { 10334 super(parent); 10335 10336 handle = CreateMenu(); 10337 tabStop = false; 10338 } 10339 } else version(custom_widgets) { 10340 /// 10341 this(Widget parent = null) { 10342 tabStop = false; // these are selected some other way 10343 super(parent); 10344 } 10345 10346 mixin Padding!q{2}; 10347 } else static assert(false); 10348 10349 version(custom_widgets) 10350 override void paint(WidgetPainter painter) { 10351 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10352 } 10353 10354 /// 10355 MenuItem addItem(MenuItem item) { 10356 this.addChild(item); 10357 items ~= item; 10358 version(win32_widgets) { 10359 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10360 } 10361 return item; 10362 } 10363 10364 10365 /// 10366 Menu addItem(Menu item) { 10367 10368 subMenus ~= item; 10369 10370 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10371 10372 addChild(mbItem); 10373 items ~= mbItem; 10374 10375 version(win32_widgets) { 10376 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10377 } else version(custom_widgets) { 10378 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10379 item.popup(mbItem); 10380 }; 10381 } else static assert(false); 10382 10383 return item; 10384 } 10385 10386 override void recomputeChildLayout() { 10387 .recomputeChildLayout!"width"(this); 10388 } 10389 10390 override int maxHeight() { return defaultLineHeight + 4; } 10391 override int minHeight() { return defaultLineHeight + 4; } 10392 } 10393 10394 10395 /** 10396 Status bars appear at the bottom of a MainWindow. 10397 They are made out of Parts, with a width and content. 10398 10399 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10400 10401 10402 sb.parts[0].content = "Status bar text!"; 10403 */ 10404 class StatusBar : Widget { 10405 private Part[] partsArray; 10406 /// 10407 struct Parts { 10408 @disable this(); 10409 this(StatusBar owner) { this.owner = owner; } 10410 //@disable this(this); 10411 /// 10412 @property int length() { return cast(int) owner.partsArray.length; } 10413 private StatusBar owner; 10414 private this(StatusBar owner, Part[] parts) { 10415 this.owner.partsArray = parts; 10416 this.owner = owner; 10417 } 10418 /// 10419 Part opIndex(int p) { 10420 if(owner.partsArray.length == 0) 10421 this ~= new StatusBar.Part(0); 10422 return owner.partsArray[p]; 10423 } 10424 10425 /// 10426 Part opOpAssign(string op : "~" )(Part p) { 10427 assert(owner.partsArray.length < 255); 10428 p.owner = this.owner; 10429 p.idx = cast(int) owner.partsArray.length; 10430 owner.partsArray ~= p; 10431 10432 owner.queueRecomputeChildLayout(); 10433 10434 version(win32_widgets) { 10435 int[256] pos; 10436 int cpos; 10437 foreach(idx, part; owner.partsArray) { 10438 if(idx + 1 == owner.partsArray.length) 10439 pos[idx] = -1; 10440 else { 10441 cpos += part.currentlyAssignedWidth; 10442 pos[idx] = cpos; 10443 } 10444 } 10445 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10446 } else version(custom_widgets) { 10447 owner.redraw(); 10448 } else static assert(false); 10449 10450 return p; 10451 } 10452 } 10453 10454 private Parts _parts; 10455 /// 10456 final @property Parts parts() { 10457 return _parts; 10458 } 10459 10460 /++ 10461 10462 +/ 10463 static class Part { 10464 /++ 10465 History: 10466 Added September 1, 2023 (dub v11.1) 10467 +/ 10468 enum WidthUnits { 10469 /++ 10470 Unscaled pixels as they appear on screen. 10471 10472 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 10473 +/ 10474 DeviceDependentPixels, 10475 /++ 10476 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 10477 +/ 10478 DeviceIndependentPixels, 10479 /++ 10480 An approximate character count in the currently selected font (at layout time) of the status bar. This will use the x-width (similar to css `ch`). 10481 +/ 10482 ApproximateCharacters, 10483 /++ 10484 These take a proportion of the remaining space in the window after all other parts have been assigned. The sum of all proportional parts is then divided by the current item to get the amount of space it uses. 10485 10486 If you pass 0, it will assume that this item takes an average of all remaining proportional space. This is there primarily to provide compatibility with code written against older versions of minigui. 10487 +/ 10488 Proportional 10489 } 10490 private WidthUnits units; 10491 private int width; 10492 private StatusBar owner; 10493 10494 private int currentlyAssignedWidth; 10495 10496 /++ 10497 History: 10498 Prior to September 1, 2023, this took a default value of 100 and was interpreted as pixels, unless the value was 0 and it was the last item in the list, in which case it would use the remaining space in the window. 10499 10500 It now allows you to provide your own value for [WidthUnits]. 10501 10502 Additionally, the default value used to be an arbitrary value of 100. It is now 0, to take advantage of the automatic proportional calculator in the new version. If you want the old behavior, pass `100, StatusBar.Part.WidthUnits.DeviceIndependentPixels`. 10503 +/ 10504 this(int w, WidthUnits units = WidthUnits.Proportional) { 10505 this.units = units; 10506 this.width = w; 10507 } 10508 10509 /// ditto 10510 this(int w = 0) { 10511 if(w == 0) 10512 this(w, WidthUnits.Proportional); 10513 else 10514 this(w, WidthUnits.DeviceDependentPixels); 10515 } 10516 10517 private int idx; 10518 private string _content; 10519 /// 10520 @property string content() { return _content; } 10521 /// 10522 @property void content(string s) { 10523 version(win32_widgets) { 10524 _content = s; 10525 WCharzBuffer bfr = WCharzBuffer(s); 10526 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10527 } else version(custom_widgets) { 10528 if(_content != s) { 10529 _content = s; 10530 owner.redraw(); 10531 } 10532 } else static assert(false); 10533 } 10534 } 10535 string simpleModeContent; 10536 bool inSimpleMode; 10537 10538 10539 /// 10540 this(Widget parent) { 10541 super(null); // FIXME 10542 _parts = Parts(this); 10543 tabStop = false; 10544 version(win32_widgets) { 10545 parentWindow = parent.parentWindow; 10546 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10547 10548 RECT rect; 10549 GetWindowRect(hwnd, &rect); 10550 idealHeight = rect.bottom - rect.top; 10551 assert(idealHeight); 10552 } else version(custom_widgets) { 10553 } else static assert(false); 10554 } 10555 10556 override void recomputeChildLayout() { 10557 int remainingLength = this.width; 10558 10559 int proportionalSum; 10560 int proportionalCount; 10561 foreach(idx, part; this.partsArray) { 10562 with(Part.WidthUnits) 10563 final switch(part.units) { 10564 case DeviceDependentPixels: 10565 part.currentlyAssignedWidth = part.width; 10566 remainingLength -= part.currentlyAssignedWidth; 10567 break; 10568 case DeviceIndependentPixels: 10569 part.currentlyAssignedWidth = scaleWithDpi(part.width); 10570 remainingLength -= part.currentlyAssignedWidth; 10571 break; 10572 case ApproximateCharacters: 10573 auto cs = getComputedStyle(); 10574 auto font = cs.font; 10575 10576 part.currentlyAssignedWidth = font.averageWidth * this.width; 10577 remainingLength -= part.currentlyAssignedWidth; 10578 break; 10579 case Proportional: 10580 proportionalSum += part.width; 10581 proportionalCount ++; 10582 break; 10583 } 10584 } 10585 10586 foreach(part; this.partsArray) { 10587 if(part.units == Part.WidthUnits.Proportional) { 10588 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 10589 if(proportion == 0) 10590 proportion = 1; 10591 10592 if(proportionalSum == 0) 10593 proportionalSum = proportionalCount; 10594 10595 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 10596 } 10597 } 10598 10599 super.recomputeChildLayout(); 10600 } 10601 10602 version(win32_widgets) 10603 override protected void dpiChanged() { 10604 RECT rect; 10605 GetWindowRect(hwnd, &rect); 10606 idealHeight = rect.bottom - rect.top; 10607 assert(idealHeight); 10608 } 10609 10610 version(custom_widgets) 10611 override void paint(WidgetPainter painter) { 10612 auto cs = getComputedStyle(); 10613 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10614 int cpos = 0; 10615 foreach(idx, part; this.partsArray) { 10616 auto partWidth = part.currentlyAssignedWidth; 10617 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10618 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10619 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10620 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10621 10622 painter.outlineColor = cs.foregroundColor(); 10623 painter.fillColor = cs.foregroundColor(); 10624 10625 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10626 cpos += partWidth; 10627 } 10628 } 10629 10630 10631 version(win32_widgets) { 10632 private int idealHeight; 10633 override int maxHeight() { return idealHeight; } 10634 override int minHeight() { return idealHeight; } 10635 } else version(custom_widgets) { 10636 override int maxHeight() { return defaultLineHeight + 4; } 10637 override int minHeight() { return defaultLineHeight + 4; } 10638 } else static assert(false); 10639 } 10640 10641 /// Displays an in-progress indicator without known values 10642 version(none) 10643 class IndefiniteProgressBar : Widget { 10644 version(win32_widgets) 10645 this(Widget parent) { 10646 super(parent); 10647 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10648 tabStop = false; 10649 } 10650 override int minHeight() { return 10; } 10651 } 10652 10653 /// A progress bar with a known endpoint and completion amount 10654 class ProgressBar : Widget { 10655 /++ 10656 History: 10657 Added March 16, 2022 (dub v10.7) 10658 +/ 10659 this(int min, int max, Widget parent) { 10660 this(parent); 10661 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10662 } 10663 this(Widget parent) { 10664 version(win32_widgets) { 10665 super(parent); 10666 createWin32Window(this, "msctls_progress32"w, "", 0); 10667 tabStop = false; 10668 } else version(custom_widgets) { 10669 super(parent); 10670 max = 100; 10671 step = 10; 10672 tabStop = false; 10673 } else static assert(0); 10674 } 10675 10676 version(custom_widgets) 10677 override void paint(WidgetPainter painter) { 10678 auto cs = getComputedStyle(); 10679 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10680 painter.fillColor = cs.progressBarColor; 10681 painter.drawRectangle(Point(0, 0), width * current / max, height); 10682 } 10683 10684 10685 version(custom_widgets) { 10686 int current; 10687 int max; 10688 int step; 10689 } 10690 10691 /// 10692 void advanceOneStep() { 10693 version(win32_widgets) 10694 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10695 else version(custom_widgets) 10696 addToPosition(step); 10697 else static assert(false); 10698 } 10699 10700 /// 10701 void setStepIncrement(int increment) { 10702 version(win32_widgets) 10703 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10704 else version(custom_widgets) 10705 step = increment; 10706 else static assert(false); 10707 } 10708 10709 /// 10710 void addToPosition(int amount) { 10711 version(win32_widgets) 10712 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10713 else version(custom_widgets) 10714 setPosition(current + amount); 10715 else static assert(false); 10716 } 10717 10718 /// 10719 void setPosition(int pos) { 10720 version(win32_widgets) 10721 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10722 else version(custom_widgets) { 10723 current = pos; 10724 if(current > max) 10725 current = max; 10726 redraw(); 10727 } 10728 else static assert(false); 10729 } 10730 10731 /// 10732 void setRange(ushort min, ushort max) { 10733 version(win32_widgets) 10734 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10735 else version(custom_widgets) { 10736 this.max = max; 10737 } 10738 else static assert(false); 10739 } 10740 10741 override int minHeight() { return 10; } 10742 } 10743 10744 version(custom_widgets) 10745 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10746 thisLabel.reserve(label.length); 10747 bool justSawAmpersand; 10748 foreach(ch; label) { 10749 if(justSawAmpersand) { 10750 justSawAmpersand = false; 10751 if(ch == '&') { 10752 goto plain; 10753 } 10754 thisAccelerator = ch; 10755 } else { 10756 if(ch == '&') { 10757 justSawAmpersand = true; 10758 continue; 10759 } 10760 plain: 10761 thisLabel ~= ch; 10762 } 10763 } 10764 } 10765 10766 /++ 10767 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. 10768 10769 10770 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 10771 10772 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10773 10774 History: 10775 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. 10776 +/ 10777 class Fieldset : Widget { 10778 // FIXME: on Windows,it doesn't draw the background on the label 10779 // on X, it doesn't fix the clipping rectangle for it 10780 version(win32_widgets) 10781 override int paddingTop() { return defaultLineHeight; } 10782 else version(custom_widgets) 10783 override int paddingTop() { return defaultLineHeight + 2; } 10784 else static assert(false); 10785 override int paddingBottom() { return 6; } 10786 override int paddingLeft() { return 6; } 10787 override int paddingRight() { return 6; } 10788 10789 override int marginLeft() { return 6; } 10790 override int marginRight() { return 6; } 10791 override int marginTop() { return 2; } 10792 override int marginBottom() { return 2; } 10793 10794 string legend; 10795 10796 version(custom_widgets) private dchar accelerator; 10797 10798 this(string legend, Widget parent) { 10799 version(win32_widgets) { 10800 super(parent); 10801 this.legend = legend; 10802 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10803 tabStop = false; 10804 } else version(custom_widgets) { 10805 super(parent); 10806 tabStop = false; 10807 10808 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10809 } else static assert(0); 10810 } 10811 10812 version(custom_widgets) 10813 override void paint(WidgetPainter painter) { 10814 auto dlh = defaultLineHeight; 10815 10816 painter.fillColor = Color.transparent; 10817 auto cs = getComputedStyle(); 10818 painter.pen = Pen(cs.foregroundColor, 1); 10819 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 10820 10821 auto tx = painter.textSize(legend); 10822 painter.outlineColor = Color.transparent; 10823 10824 version(Windows) { 10825 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10826 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10827 SelectObject(painter.impl.hdc, b); 10828 } else static if(UsingSimpledisplayX11) { 10829 painter.fillColor = getComputedStyle().windowBackgroundColor; 10830 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10831 } 10832 painter.outlineColor = cs.foregroundColor; 10833 painter.drawText(Point(8, 0), legend); 10834 } 10835 10836 override int maxHeight() { 10837 auto m = paddingTop() + paddingBottom(); 10838 foreach(child; children) { 10839 auto mh = child.maxHeight(); 10840 if(mh == int.max) 10841 return int.max; 10842 m += mh; 10843 m += child.marginBottom(); 10844 m += child.marginTop(); 10845 } 10846 m += 6; 10847 if(m < minHeight) 10848 return minHeight; 10849 return m; 10850 } 10851 10852 override int minHeight() { 10853 auto m = paddingTop() + paddingBottom(); 10854 foreach(child; children) { 10855 m += child.minHeight(); 10856 m += child.marginBottom(); 10857 m += child.marginTop(); 10858 } 10859 return m + 6; 10860 } 10861 10862 override int minWidth() { 10863 return 6 + cast(int) this.legend.length * 7; 10864 } 10865 } 10866 10867 /++ 10868 $(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") 10869 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10870 +/ 10871 version(minigui_screenshots) 10872 @Screenshot("Fieldset") 10873 unittest { 10874 auto window = new Window(200, 100); 10875 auto set = new Fieldset("Baby will", window); 10876 auto option1 = new Radiobox("Eat", set); 10877 auto option2 = new Radiobox("Cry", set); 10878 auto option3 = new Radiobox("Sleep", set); 10879 window.loop(); 10880 } 10881 10882 /// Draws a line 10883 class HorizontalRule : Widget { 10884 mixin Margin!q{ 2 }; 10885 override int minHeight() { return 2; } 10886 override int maxHeight() { return 2; } 10887 10888 /// 10889 this(Widget parent) { 10890 super(parent); 10891 } 10892 10893 override void paint(WidgetPainter painter) { 10894 auto cs = getComputedStyle(); 10895 painter.outlineColor = cs.darkAccentColor; 10896 painter.drawLine(Point(0, 0), Point(width, 0)); 10897 painter.outlineColor = cs.lightAccentColor; 10898 painter.drawLine(Point(0, 1), Point(width, 1)); 10899 } 10900 } 10901 10902 version(minigui_screenshots) 10903 @Screenshot("HorizontalRule") 10904 /++ 10905 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10906 10907 +/ 10908 unittest { 10909 auto window = new Window(200, 100); 10910 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10911 new HorizontalRule(window); 10912 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10913 window.loop(); 10914 } 10915 10916 /// ditto 10917 class VerticalRule : Widget { 10918 mixin Margin!q{ 2 }; 10919 override int minWidth() { return 2; } 10920 override int maxWidth() { return 2; } 10921 10922 /// 10923 this(Widget parent) { 10924 super(parent); 10925 } 10926 10927 override void paint(WidgetPainter painter) { 10928 auto cs = getComputedStyle(); 10929 painter.outlineColor = cs.darkAccentColor; 10930 painter.drawLine(Point(0, 0), Point(0, height)); 10931 painter.outlineColor = cs.lightAccentColor; 10932 painter.drawLine(Point(1, 0), Point(1, height)); 10933 } 10934 } 10935 10936 10937 /// 10938 class Menu : Window { 10939 void remove() { 10940 foreach(i, child; parentWindow.children) 10941 if(child is this) { 10942 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10943 break; 10944 } 10945 parentWindow.redraw(); 10946 10947 parentWindow.releaseMouseCapture(); 10948 } 10949 10950 /// 10951 void addSeparator() { 10952 version(win32_widgets) 10953 AppendMenu(handle, MF_SEPARATOR, 0, null); 10954 else version(custom_widgets) 10955 auto hr = new HorizontalRule(this); 10956 else static assert(0); 10957 } 10958 10959 override int paddingTop() { return 4; } 10960 override int paddingBottom() { return 4; } 10961 override int paddingLeft() { return 2; } 10962 override int paddingRight() { return 2; } 10963 10964 version(win32_widgets) {} 10965 else version(custom_widgets) { 10966 SimpleWindow dropDown; 10967 Widget menuParent; 10968 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10969 this.menuParent = parent; 10970 10971 int w = 150; 10972 int h = paddingTop + paddingBottom; 10973 if(this.children.length) { 10974 // hacking it to get the ideal height out of recomputeChildLayout 10975 this.width = w; 10976 this.height = h; 10977 this.recomputeChildLayoutEntry(); 10978 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10979 h += paddingBottom; 10980 10981 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 10982 } 10983 10984 if(offsetY == int.min) 10985 offsetY = parent.defaultLineHeight; 10986 10987 auto coord = parent.globalCoordinates(); 10988 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10989 this.x = 0; 10990 this.y = 0; 10991 this.width = dropDown.width; 10992 this.height = dropDown.height; 10993 this.drawableWindow = dropDown; 10994 this.recomputeChildLayoutEntry(); 10995 10996 static if(UsingSimpledisplayX11) 10997 XSync(XDisplayConnection.get, 0); 10998 10999 dropDown.visibilityChanged = (bool visible) { 11000 if(visible) { 11001 this.redraw(); 11002 dropDown.grabInput(); 11003 } else { 11004 dropDown.releaseInputGrab(); 11005 } 11006 }; 11007 11008 dropDown.show(); 11009 11010 clickListener = this.addEventListener((scope ClickEvent ev) { 11011 unpopup(); 11012 // need to unlock asap just in case other user handlers block... 11013 static if(UsingSimpledisplayX11) 11014 flushGui(); 11015 }, true /* again for asap action */); 11016 } 11017 11018 EventListener clickListener; 11019 } 11020 else static assert(false); 11021 11022 version(custom_widgets) 11023 void unpopup() { 11024 mouseLastOver = mouseLastDownOn = null; 11025 dropDown.hide(); 11026 if(!menuParent.parentWindow.win.closed) { 11027 if(auto maw = cast(MouseActivatedWidget) menuParent) { 11028 maw.setDynamicState(DynamicState.depressed, false); 11029 maw.setDynamicState(DynamicState.hover, false); 11030 maw.redraw(); 11031 } 11032 // menuParent.parentWindow.win.focus(); 11033 } 11034 clickListener.disconnect(); 11035 } 11036 11037 MenuItem[] items; 11038 11039 /// 11040 MenuItem addItem(MenuItem item) { 11041 addChild(item); 11042 items ~= item; 11043 version(win32_widgets) { 11044 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11045 } 11046 return item; 11047 } 11048 11049 string label; 11050 11051 version(win32_widgets) { 11052 HMENU handle; 11053 /// 11054 this(string label, Widget parent) { 11055 // not actually passing the parent since it effs up the drawing 11056 super(cast(Widget) null);// parent); 11057 this.label = label; 11058 handle = CreatePopupMenu(); 11059 } 11060 } else version(custom_widgets) { 11061 /// 11062 this(string label, Widget parent) { 11063 11064 if(dropDown) { 11065 dropDown.close(); 11066 } 11067 dropDown = new SimpleWindow( 11068 150, 4, 11069 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11070 11071 this.label = label; 11072 11073 super(dropDown); 11074 } 11075 } else static assert(false); 11076 11077 override int maxHeight() { return defaultLineHeight; } 11078 override int minHeight() { return defaultLineHeight; } 11079 11080 version(custom_widgets) 11081 override void paint(WidgetPainter painter) { 11082 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 11083 } 11084 } 11085 11086 /++ 11087 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 11088 +/ 11089 class MenuItem : MouseActivatedWidget { 11090 Menu submenu; 11091 11092 Action action; 11093 string label; 11094 11095 override int paddingLeft() { return 4; } 11096 11097 override int maxHeight() { return defaultLineHeight + 4; } 11098 override int minHeight() { return defaultLineHeight + 4; } 11099 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 11100 override int maxWidth() { 11101 if(cast(MenuBar) parent) { 11102 return minWidth(); 11103 } 11104 return int.max; 11105 } 11106 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 11107 this(string lbl, Widget parent = null) { 11108 super(parent); 11109 //label = lbl; // FIXME 11110 foreach(char ch; lbl) // FIXME 11111 if(ch != '&') // FIXME 11112 label ~= ch; // FIXME 11113 tabStop = false; // these are selected some other way 11114 } 11115 11116 /// 11117 this(Action action, Widget parent = null) { 11118 assert(action !is null); 11119 this(action.label, parent); 11120 this.action = action; 11121 tabStop = false; // these are selected some other way 11122 } 11123 11124 version(custom_widgets) 11125 override void paint(WidgetPainter painter) { 11126 auto cs = getComputedStyle(); 11127 if(dynamicState & DynamicState.depressed) 11128 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11129 if(dynamicState & DynamicState.hover) 11130 painter.outlineColor = cs.activeMenuItemColor; 11131 else 11132 painter.outlineColor = cs.foregroundColor; 11133 painter.fillColor = Color.transparent; 11134 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11135 if(action && action.accelerator !is KeyEvent.init) { 11136 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 11137 11138 } 11139 } 11140 11141 static class Style : Widget.Style { 11142 override bool variesWithState(ulong dynamicStateFlags) { 11143 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11144 } 11145 } 11146 mixin OverrideStyle!Style; 11147 11148 override void defaultEventHandler_triggered(Event event) { 11149 if(action) 11150 foreach(handler; action.triggered) 11151 handler(); 11152 11153 if(auto pmenu = cast(Menu) this.parent) 11154 pmenu.remove(); 11155 11156 super.defaultEventHandler_triggered(event); 11157 } 11158 } 11159 11160 version(win32_widgets) 11161 /// A "mouse activiated widget" is really just an abstract variant of button. 11162 class MouseActivatedWidget : Widget { 11163 @property bool isChecked() { 11164 assert(hwnd); 11165 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 11166 11167 } 11168 @property void isChecked(bool state) { 11169 assert(hwnd); 11170 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 11171 11172 } 11173 11174 override void handleWmCommand(ushort cmd, ushort id) { 11175 if(cmd == 0) { 11176 auto event = new Event(EventType.triggered, this); 11177 event.dispatch(); 11178 } 11179 } 11180 11181 this(Widget parent) { 11182 super(parent); 11183 } 11184 } 11185 else version(custom_widgets) 11186 /// ditto 11187 class MouseActivatedWidget : Widget { 11188 @property bool isChecked() { return isChecked_; } 11189 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 11190 11191 private bool isChecked_; 11192 11193 this(Widget parent) { 11194 super(parent); 11195 11196 addEventListener((MouseDownEvent ev) { 11197 if(ev.button == MouseButton.left) { 11198 setDynamicState(DynamicState.depressed, true); 11199 setDynamicState(DynamicState.hover, true); 11200 redraw(); 11201 } 11202 }); 11203 11204 addEventListener((MouseUpEvent ev) { 11205 if(ev.button == MouseButton.left) { 11206 setDynamicState(DynamicState.depressed, false); 11207 setDynamicState(DynamicState.hover, false); 11208 redraw(); 11209 } 11210 }); 11211 11212 addEventListener((MouseMoveEvent mme) { 11213 if(!(mme.state & ModifierState.leftButtonDown)) { 11214 if(dynamicState_ & DynamicState.depressed) { 11215 setDynamicState(DynamicState.depressed, false); 11216 redraw(); 11217 } 11218 } 11219 }); 11220 } 11221 11222 override void defaultEventHandler_focus(Event ev) { 11223 super.defaultEventHandler_focus(ev); 11224 this.redraw(); 11225 } 11226 override void defaultEventHandler_blur(Event ev) { 11227 super.defaultEventHandler_blur(ev); 11228 setDynamicState(DynamicState.depressed, false); 11229 this.redraw(); 11230 } 11231 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11232 super.defaultEventHandler_keydown(ev); 11233 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 11234 setDynamicState(DynamicState.depressed, true); 11235 setDynamicState(DynamicState.hover, true); 11236 this.redraw(); 11237 } 11238 } 11239 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11240 super.defaultEventHandler_keyup(ev); 11241 if(!(dynamicState & DynamicState.depressed)) 11242 return; 11243 setDynamicState(DynamicState.depressed, false); 11244 setDynamicState(DynamicState.hover, false); 11245 this.redraw(); 11246 11247 auto event = new Event(EventType.triggered, this); 11248 event.sendDirectly(); 11249 } 11250 override void defaultEventHandler_click(ClickEvent ev) { 11251 super.defaultEventHandler_click(ev); 11252 if(ev.button == MouseButton.left) { 11253 auto event = new Event(EventType.triggered, this); 11254 event.sendDirectly(); 11255 } 11256 } 11257 11258 } 11259 else static assert(false); 11260 11261 /* 11262 /++ 11263 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11264 11265 Basically the same as a checkbox. 11266 +/ 11267 class OnOffSwitch : MouseActivatedWidget { 11268 11269 } 11270 */ 11271 11272 /++ 11273 History: 11274 Added June 15, 2021 (dub v10.1) 11275 +/ 11276 struct ImageLabel { 11277 /++ 11278 Defines a label+image combo used by some widgets. 11279 11280 If you provide just a text label, that is all the widget will try to 11281 display. Or just an image will display just that. If you provide both, 11282 it may display both text and image side by side or display the image 11283 and offer text on an input event depending on the widget. 11284 11285 History: 11286 The `alignment` parameter was added on September 27, 2021 11287 +/ 11288 this(string label, TextAlignment alignment = TextAlignment.Center) { 11289 this.label = label; 11290 this.displayFlags = DisplayFlags.displayText; 11291 this.alignment = alignment; 11292 } 11293 11294 /// ditto 11295 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11296 this.label = label; 11297 this.image = image; 11298 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11299 this.alignment = alignment; 11300 } 11301 11302 /// ditto 11303 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11304 this.image = image; 11305 this.displayFlags = DisplayFlags.displayImage; 11306 this.alignment = alignment; 11307 } 11308 11309 /// ditto 11310 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11311 this.label = label; 11312 this.image = image; 11313 this.alignment = alignment; 11314 this.displayFlags = displayFlags; 11315 } 11316 11317 string label; 11318 MemoryImage image; 11319 11320 enum DisplayFlags { 11321 displayText = 1 << 0, 11322 displayImage = 1 << 1, 11323 } 11324 11325 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11326 11327 TextAlignment alignment; 11328 } 11329 11330 /++ 11331 A basic checked or not checked box with an attached label. 11332 11333 11334 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 11335 11336 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11337 11338 History: 11339 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. 11340 +/ 11341 class Checkbox : MouseActivatedWidget { 11342 version(win32_widgets) { 11343 override int maxHeight() { return scaleWithDpi(16); } 11344 override int minHeight() { return scaleWithDpi(16); } 11345 } else version(custom_widgets) { 11346 private enum buttonSize = 16; 11347 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11348 override int minHeight() { return maxHeight(); } 11349 } else static assert(0); 11350 11351 override int marginLeft() { return 4; } 11352 11353 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11354 11355 /++ 11356 Just an alias because I keep typing checked out of web habit. 11357 11358 History: 11359 Added May 31, 2021 11360 +/ 11361 alias checked = isChecked; 11362 11363 private string label; 11364 private dchar accelerator; 11365 11366 /++ 11367 +/ 11368 this(string label, Widget parent) { 11369 this(ImageLabel(label), Appearance.checkbox, parent); 11370 } 11371 11372 /// ditto 11373 this(string label, Appearance appearance, Widget parent) { 11374 this(ImageLabel(label), appearance, parent); 11375 } 11376 11377 /++ 11378 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11379 11380 History: 11381 Added June 29, 2021 (dub v10.2) 11382 +/ 11383 enum Appearance { 11384 checkbox, /// a normal checkbox 11385 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11386 //sliderswitch, 11387 } 11388 private Appearance appearance; 11389 11390 /// ditto 11391 private this(ImageLabel label, Appearance appearance, Widget parent) { 11392 super(parent); 11393 version(win32_widgets) { 11394 this.label = label.label; 11395 11396 uint extraStyle; 11397 final switch(appearance) { 11398 case Appearance.checkbox: 11399 break; 11400 case Appearance.pushbutton: 11401 extraStyle |= BS_PUSHLIKE; 11402 break; 11403 } 11404 11405 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11406 } else version(custom_widgets) { 11407 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11408 } else static assert(0); 11409 } 11410 11411 version(custom_widgets) 11412 override void paint(WidgetPainter painter) { 11413 auto cs = getComputedStyle(); 11414 if(isFocused()) { 11415 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11416 painter.fillColor = cs.windowBackgroundColor; 11417 painter.drawRectangle(Point(0, 0), width, height); 11418 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11419 } else { 11420 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11421 painter.fillColor = cs.windowBackgroundColor; 11422 painter.drawRectangle(Point(0, 0), width, height); 11423 } 11424 11425 11426 painter.outlineColor = Color.black; 11427 painter.fillColor = Color.white; 11428 enum rectOffset = 2; 11429 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 11430 11431 if(isChecked) { 11432 auto size = scaleWithDpi(2); 11433 painter.pen = Pen(Color.black, size); 11434 // I'm using height so the checkbox is square 11435 enum padding = 3; 11436 painter.drawLine( 11437 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 11438 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 11439 ); 11440 painter.drawLine( 11441 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 11442 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 11443 ); 11444 11445 painter.pen = Pen(Color.black, 1); 11446 } 11447 11448 if(label !is null) { 11449 painter.outlineColor = cs.foregroundColor(); 11450 painter.fillColor = cs.foregroundColor(); 11451 11452 // i want the centerline of the text to be aligned with the centerline of the checkbox 11453 /+ 11454 auto font = cs.font(); 11455 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 11456 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 11457 +/ 11458 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 11459 } 11460 } 11461 11462 override void defaultEventHandler_triggered(Event ev) { 11463 isChecked = !isChecked; 11464 11465 this.emit!(ChangeEvent!bool)(&isChecked); 11466 11467 redraw(); 11468 } 11469 11470 /// Emits a change event with the checked state 11471 mixin Emits!(ChangeEvent!bool); 11472 } 11473 11474 /// Adds empty space to a layout. 11475 class VerticalSpacer : Widget { 11476 /// 11477 this(Widget parent) { 11478 super(parent); 11479 } 11480 } 11481 11482 /// ditto 11483 class HorizontalSpacer : Widget { 11484 /// 11485 this(Widget parent) { 11486 super(parent); 11487 this.tabStop = false; 11488 } 11489 } 11490 11491 11492 /++ 11493 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11494 11495 11496 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 11497 11498 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11499 11500 History: 11501 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. 11502 +/ 11503 class Radiobox : MouseActivatedWidget { 11504 11505 version(win32_widgets) { 11506 override int maxHeight() { return scaleWithDpi(16); } 11507 override int minHeight() { return scaleWithDpi(16); } 11508 } else version(custom_widgets) { 11509 private enum buttonSize = 16; 11510 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11511 override int minHeight() { return maxHeight(); } 11512 } else static assert(0); 11513 11514 override int marginLeft() { return 4; } 11515 11516 // FIXME: make a label getter 11517 private string label; 11518 private dchar accelerator; 11519 11520 /++ 11521 11522 +/ 11523 this(string label, Widget parent) { 11524 super(parent); 11525 version(win32_widgets) { 11526 this.label = label; 11527 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11528 } else version(custom_widgets) { 11529 label.extractWindowsStyleLabel(this.label, this.accelerator); 11530 height = 16; 11531 width = height + 4 + cast(int) label.length * 16; 11532 } 11533 } 11534 11535 version(custom_widgets) 11536 override void paint(WidgetPainter painter) { 11537 auto cs = getComputedStyle(); 11538 11539 if(isFocused) { 11540 painter.fillColor = cs.windowBackgroundColor; 11541 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11542 } else { 11543 painter.fillColor = cs.windowBackgroundColor; 11544 painter.outlineColor = cs.windowBackgroundColor; 11545 } 11546 painter.drawRectangle(Point(0, 0), width, height); 11547 11548 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11549 11550 painter.outlineColor = Color.black; 11551 painter.fillColor = Color.white; 11552 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11553 if(isChecked) { 11554 painter.outlineColor = Color.black; 11555 painter.fillColor = Color.black; 11556 // I'm using height so the checkbox is square 11557 auto size = scaleWithDpi(2); 11558 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 11559 } 11560 11561 painter.outlineColor = cs.foregroundColor(); 11562 painter.fillColor = cs.foregroundColor(); 11563 11564 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11565 } 11566 11567 11568 override void defaultEventHandler_triggered(Event ev) { 11569 isChecked = true; 11570 11571 if(this.parent) { 11572 foreach(child; this.parent.children) { 11573 if(child is this) continue; 11574 if(auto rb = cast(Radiobox) child) { 11575 rb.isChecked = false; 11576 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11577 rb.redraw(); 11578 } 11579 } 11580 } 11581 11582 this.emit!(ChangeEvent!bool)(&this.isChecked); 11583 11584 redraw(); 11585 } 11586 11587 /// 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. 11588 mixin Emits!(ChangeEvent!bool); 11589 } 11590 11591 11592 /++ 11593 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11594 11595 11596 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 11597 11598 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11599 11600 History: 11601 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. 11602 +/ 11603 class Button : MouseActivatedWidget { 11604 override int heightStretchiness() { return 3; } 11605 override int widthStretchiness() { return 3; } 11606 11607 /++ 11608 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. 11609 11610 History: 11611 Added July 2, 2021 11612 +/ 11613 public bool triggersOnMultiClick; 11614 11615 private string label_; 11616 private TextAlignment alignment; 11617 private dchar accelerator; 11618 11619 /// 11620 string label() { return label_; } 11621 /// 11622 void label(string l) { 11623 label_ = l; 11624 version(win32_widgets) { 11625 WCharzBuffer bfr = WCharzBuffer(l); 11626 SetWindowTextW(hwnd, bfr.ptr); 11627 } else version(custom_widgets) { 11628 redraw(); 11629 } 11630 } 11631 11632 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11633 super.defaultEventHandler_dblclick(ev); 11634 if(triggersOnMultiClick) { 11635 if(ev.button == MouseButton.left) { 11636 auto event = new Event(EventType.triggered, this); 11637 event.sendDirectly(); 11638 } 11639 } 11640 } 11641 11642 private Sprite sprite; 11643 private int displayFlags; 11644 11645 /++ 11646 Creates a push button with the given label, which may be an image or some text. 11647 11648 Bugs: 11649 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11650 11651 History: 11652 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11653 11654 The button with label and image will respect requests to show both on Windows as 11655 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11656 +/ 11657 this(ImageLabel label, Widget parent) { 11658 version(win32_widgets) { 11659 // FIXME: use ideal button size instead 11660 width = 50; 11661 height = 30; 11662 super(parent); 11663 11664 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11665 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11666 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11667 11668 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11669 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11670 11671 if(label.image) { 11672 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11673 11674 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11675 } 11676 11677 this.label = label.label; 11678 } else version(custom_widgets) { 11679 width = 50; 11680 height = 30; 11681 super(parent); 11682 11683 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11684 11685 if(label.image) { 11686 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11687 this.displayFlags = label.displayFlags; 11688 } 11689 11690 this.alignment = label.alignment; 11691 } 11692 } 11693 11694 /// 11695 this(string label, Widget parent) { 11696 this(ImageLabel(label), parent); 11697 } 11698 11699 override int minHeight() { return defaultLineHeight + 4; } 11700 11701 static class Style : Widget.Style { 11702 override WidgetBackground background() { 11703 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11704 11705 auto pressed = DynamicState.depressed | DynamicState.hover; 11706 if((widget.dynamicState & pressed) == pressed) { 11707 return WidgetBackground(cs.depressedButtonColor()); 11708 } else if(widget.dynamicState & DynamicState.hover) { 11709 return WidgetBackground(cs.hoveringColor()); 11710 } else { 11711 return WidgetBackground(cs.buttonColor()); 11712 } 11713 } 11714 11715 override FrameStyle borderStyle() { 11716 auto pressed = DynamicState.depressed | DynamicState.hover; 11717 if((widget.dynamicState & pressed) == pressed) { 11718 return FrameStyle.sunk; 11719 } else { 11720 return FrameStyle.risen; 11721 } 11722 11723 } 11724 11725 override bool variesWithState(ulong dynamicStateFlags) { 11726 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11727 } 11728 } 11729 mixin OverrideStyle!Style; 11730 11731 version(custom_widgets) 11732 override void paint(WidgetPainter painter) { 11733 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11734 if(sprite) { 11735 sprite.drawAt( 11736 painter, 11737 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11738 Point(0, 0) 11739 ); 11740 } else { 11741 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11742 } 11743 return bounds; 11744 }); 11745 } 11746 11747 override int flexBasisWidth() { 11748 version(win32_widgets) { 11749 SIZE size; 11750 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11751 if(size.cx == 0) 11752 goto fallback; 11753 return size.cx + scaleWithDpi(16); 11754 } 11755 fallback: 11756 return scaleWithDpi(cast(int) label.length * 8 + 16); 11757 } 11758 11759 override int flexBasisHeight() { 11760 version(win32_widgets) { 11761 SIZE size; 11762 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11763 if(size.cy == 0) 11764 goto fallback; 11765 return size.cy + scaleWithDpi(6); 11766 } 11767 fallback: 11768 return defaultLineHeight + 4; 11769 } 11770 } 11771 11772 /++ 11773 A button with a consistent size, suitable for user commands like OK and CANCEL. 11774 +/ 11775 class CommandButton : Button { 11776 this(string label, Widget parent) { 11777 super(label, parent); 11778 } 11779 11780 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11781 11782 override int maxHeight() { 11783 return defaultLineHeight + 4; 11784 } 11785 11786 override int maxWidth() { 11787 return defaultLineHeight * 4; 11788 } 11789 11790 override int marginLeft() { return 12; } 11791 override int marginRight() { return 12; } 11792 override int marginTop() { return 12; } 11793 override int marginBottom() { return 12; } 11794 } 11795 11796 /// 11797 enum ArrowDirection { 11798 left, /// 11799 right, /// 11800 up, /// 11801 down /// 11802 } 11803 11804 /// 11805 version(custom_widgets) 11806 class ArrowButton : Button { 11807 /// 11808 this(ArrowDirection direction, Widget parent) { 11809 super("", parent); 11810 this.direction = direction; 11811 triggersOnMultiClick = true; 11812 } 11813 11814 private ArrowDirection direction; 11815 11816 override int minHeight() { return scaleWithDpi(16); } 11817 override int maxHeight() { return scaleWithDpi(16); } 11818 override int minWidth() { return scaleWithDpi(16); } 11819 override int maxWidth() { return scaleWithDpi(16); } 11820 11821 override void paint(WidgetPainter painter) { 11822 super.paint(painter); 11823 11824 auto cs = getComputedStyle(); 11825 11826 painter.outlineColor = cs.foregroundColor; 11827 painter.fillColor = cs.foregroundColor; 11828 11829 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11830 11831 final switch(direction) { 11832 case ArrowDirection.up: 11833 painter.drawPolygon( 11834 scaleWithDpi(Point(2, 10) + offset), 11835 scaleWithDpi(Point(7, 5) + offset), 11836 scaleWithDpi(Point(12, 10) + offset), 11837 scaleWithDpi(Point(2, 10) + offset) 11838 ); 11839 break; 11840 case ArrowDirection.down: 11841 painter.drawPolygon( 11842 scaleWithDpi(Point(2, 6) + offset), 11843 scaleWithDpi(Point(7, 11) + offset), 11844 scaleWithDpi(Point(12, 6) + offset), 11845 scaleWithDpi(Point(2, 6) + offset) 11846 ); 11847 break; 11848 case ArrowDirection.left: 11849 painter.drawPolygon( 11850 scaleWithDpi(Point(10, 2) + offset), 11851 scaleWithDpi(Point(5, 7) + offset), 11852 scaleWithDpi(Point(10, 12) + offset), 11853 scaleWithDpi(Point(10, 2) + offset) 11854 ); 11855 break; 11856 case ArrowDirection.right: 11857 painter.drawPolygon( 11858 scaleWithDpi(Point(6, 2) + offset), 11859 scaleWithDpi(Point(11, 7) + offset), 11860 scaleWithDpi(Point(6, 12) + offset), 11861 scaleWithDpi(Point(6, 2) + offset) 11862 ); 11863 break; 11864 } 11865 } 11866 } 11867 11868 private 11869 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11870 int x, y; 11871 Widget par = c; 11872 while(par) { 11873 x += par.x; 11874 y += par.y; 11875 par = par.parent; 11876 } 11877 return [x, y]; 11878 } 11879 11880 version(win32_widgets) 11881 private 11882 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11883 // MapWindowPoints? 11884 int x, y; 11885 Widget par = c; 11886 while(par) { 11887 x += par.x; 11888 y += par.y; 11889 par = par.parent; 11890 if(par !is null && par.useNativeDrawing()) 11891 break; 11892 } 11893 return [x, y]; 11894 } 11895 11896 /// 11897 class ImageBox : Widget { 11898 private MemoryImage image_; 11899 11900 override int widthStretchiness() { return 1; } 11901 override int heightStretchiness() { return 1; } 11902 override int widthShrinkiness() { return 1; } 11903 override int heightShrinkiness() { return 1; } 11904 11905 override int flexBasisHeight() { 11906 return image_.height; 11907 } 11908 11909 override int flexBasisWidth() { 11910 return image_.width; 11911 } 11912 11913 /// 11914 public void setImage(MemoryImage image){ 11915 this.image_ = image; 11916 if(this.parentWindow && this.parentWindow.win) { 11917 if(sprite) 11918 sprite.dispose(); 11919 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11920 } 11921 redraw(); 11922 } 11923 11924 /// How to fit the image in the box if they aren't an exact match in size? 11925 enum HowToFit { 11926 center, /// centers the image, cropping around all the edges as needed 11927 crop, /// always draws the image in the upper left, cropping the lower right if needed 11928 // stretch, /// not implemented 11929 } 11930 11931 private Sprite sprite; 11932 private HowToFit howToFit_; 11933 11934 private Color backgroundColor_; 11935 11936 /// 11937 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11938 this.image_ = image; 11939 this.tabStop = false; 11940 this.howToFit_ = howToFit; 11941 this.backgroundColor_ = backgroundColor; 11942 super(parent); 11943 updateSprite(); 11944 } 11945 11946 /// ditto 11947 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11948 this(image, howToFit, Color.transparent, parent); 11949 } 11950 11951 private void updateSprite() { 11952 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11953 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11954 } 11955 } 11956 11957 override void paint(WidgetPainter painter) { 11958 updateSprite(); 11959 if(backgroundColor_.a) { 11960 painter.fillColor = backgroundColor_; 11961 painter.drawRectangle(Point(0, 0), width, height); 11962 } 11963 if(howToFit_ == HowToFit.crop) 11964 sprite.drawAt(painter, Point(0, 0)); 11965 else if(howToFit_ == HowToFit.center) { 11966 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11967 } 11968 } 11969 } 11970 11971 /// 11972 class TextLabel : Widget { 11973 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 11974 override int maxHeight() { return minHeight; } 11975 override int minWidth() { return 32; } 11976 11977 override int flexBasisHeight() { return minHeight(); } 11978 override int flexBasisWidth() { return defaultTextWidth(label); } 11979 11980 string label_; 11981 11982 /++ 11983 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11984 11985 In practice this means a click on the label will focus the `labelFor`. In future versions 11986 it will also set screen reader hints but that is not yet implemented. 11987 11988 History: 11989 Added October 3, 2021 (dub v10.4) 11990 +/ 11991 Widget labelFor; 11992 11993 /// 11994 @scriptable 11995 string label() { return label_; } 11996 11997 /// 11998 @scriptable 11999 void label(string l) { 12000 label_ = l; 12001 version(win32_widgets) { 12002 WCharzBuffer bfr = WCharzBuffer(l); 12003 SetWindowTextW(hwnd, bfr.ptr); 12004 } else version(custom_widgets) 12005 redraw(); 12006 } 12007 12008 override void defaultEventHandler_click(scope ClickEvent ce) { 12009 if(this.labelFor !is null) 12010 this.labelFor.focus(); 12011 } 12012 12013 /++ 12014 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 12015 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 12016 +/ 12017 this(string label, TextAlignment alignment, Widget parent) { 12018 this.label_ = label; 12019 this.alignment = alignment; 12020 this.tabStop = false; 12021 super(parent); 12022 12023 version(win32_widgets) 12024 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 12025 } 12026 12027 /// ditto 12028 this(string label, Widget parent) { 12029 this(label, TextAlignment.Right, parent); 12030 } 12031 12032 TextAlignment alignment; 12033 12034 version(custom_widgets) 12035 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12036 painter.outlineColor = getComputedStyle().foregroundColor; 12037 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 12038 return bounds; 12039 } 12040 12041 } 12042 12043 version(custom_widgets) 12044 private struct etc { 12045 mixin ExperimentalTextComponent; 12046 } 12047 12048 version(win32_widgets) 12049 alias EditableTextWidgetParent = Widget; /// 12050 else version(custom_widgets) { 12051 version(trash_text) { 12052 alias EditableTextWidgetParent = ScrollableWidget; /// 12053 } else { 12054 alias EditableTextWidgetParent = Widget; 12055 version=use_new_text_system; 12056 import arsd.textlayouter; 12057 } 12058 } else static assert(0); 12059 12060 version(use_new_text_system) 12061 class TextDisplayHelper : Widget { 12062 protected TextLayouter l; 12063 protected ScrollMessageWidget smw; 12064 12065 private const(TextLayouter.State)*[] undoStack; 12066 private const(TextLayouter.State)*[] redoStack; 12067 12068 bool readonly; 12069 bool caretNavigation; // scroll lock can flip this 12070 bool singleLine; 12071 bool acceptsTabInput; 12072 12073 private Menu ctx; 12074 override Menu contextMenu(int x, int y) { 12075 if(ctx is null) { 12076 ctx = new Menu("Actions", this); 12077 if(!readonly) { 12078 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 12079 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 12080 ctx.addSeparator(); 12081 } 12082 if(!readonly) 12083 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 12084 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 12085 if(!readonly) 12086 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 12087 if(!readonly) 12088 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 12089 ctx.addSeparator(); 12090 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 12091 } 12092 return ctx; 12093 } 12094 12095 override void defaultEventHandler_blur(Event ev) { 12096 super.defaultEventHandler_blur(ev); 12097 if(l.wasMutated()) { 12098 auto evt = new ChangeEvent!string(this, &this.content); 12099 evt.dispatch(); 12100 l.clearWasMutatedFlag(); 12101 } 12102 } 12103 12104 private string content() { 12105 return l.getTextString(); 12106 } 12107 12108 void undo() { 12109 if(readonly) return; 12110 if(undoStack.length) { 12111 auto state = undoStack[$-1]; 12112 undoStack = undoStack[0 .. $-1]; 12113 undoStack.assumeSafeAppend(); 12114 redoStack ~= l.saveState(); 12115 l.restoreState(state); 12116 adjustScrollbarSizes(); 12117 scrollForCaret(); 12118 redraw(); 12119 stateCheckpoint = true; 12120 } 12121 } 12122 12123 void redo() { 12124 if(readonly) return; 12125 if(redoStack.length) { 12126 doStateCheckpoint(); 12127 auto state = redoStack[$-1]; 12128 redoStack = redoStack[0 .. $-1]; 12129 redoStack.assumeSafeAppend(); 12130 l.restoreState(state); 12131 adjustScrollbarSizes(); 12132 scrollForCaret(); 12133 redraw(); 12134 stateCheckpoint = true; 12135 } 12136 } 12137 12138 void cut() { 12139 if(readonly) return; 12140 with(l.selection()) { 12141 if(!isEmpty()) { 12142 setClipboardText(parentWindow.win, getContentString()); 12143 doStateCheckpoint(); 12144 replaceContent(""); 12145 adjustScrollbarSizes(); 12146 scrollForCaret(); 12147 this.redraw(); 12148 } 12149 } 12150 12151 } 12152 12153 void copy() { 12154 with(l.selection()) { 12155 if(!isEmpty()) { 12156 setClipboardText(parentWindow.win, getContentString()); 12157 this.redraw(); 12158 } 12159 } 12160 } 12161 12162 void paste() { 12163 if(readonly) return; 12164 getClipboardText(parentWindow.win, (txt) { 12165 doStateCheckpoint(); 12166 l.selection.replaceContent(txt); 12167 adjustScrollbarSizes(); 12168 scrollForCaret(); 12169 this.redraw(); 12170 }); 12171 } 12172 12173 void deleteContentOfSelection() { 12174 if(readonly) return; 12175 doStateCheckpoint(); 12176 l.selection.replaceContent(""); 12177 l.selection.setUserXCoordinate(); 12178 adjustScrollbarSizes(); 12179 scrollForCaret(); 12180 redraw(); 12181 } 12182 12183 void selectAll() { 12184 with(l.selection) { 12185 moveToStartOfDocument(); 12186 setAnchor(); 12187 moveToEndOfDocument(); 12188 setFocus(); 12189 } 12190 redraw(); 12191 } 12192 12193 protected bool stateCheckpoint = true; 12194 12195 protected void doStateCheckpoint() { 12196 if(stateCheckpoint) { 12197 undoStack ~= l.saveState(); 12198 stateCheckpoint = false; 12199 } 12200 } 12201 12202 protected void adjustScrollbarSizes() { 12203 // FIXME: will want a content area helper function instead of doing all these subtractions myself 12204 auto borderWidth = 2; 12205 this.smw.setTotalArea(l.width, l.height); 12206 this.smw.setViewableArea( 12207 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 12208 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 12209 } 12210 12211 protected void scrollForCaret() { 12212 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 12213 smw.scrollIntoView(l.selection.focusBoundingBox()); 12214 } 12215 12216 // FIXME: this should be a theme changed event listener instead 12217 private BaseVisualTheme currentTheme; 12218 override void recomputeChildLayout() { 12219 if(currentTheme is null) 12220 currentTheme = WidgetPainter.visualTheme; 12221 if(WidgetPainter.visualTheme !is currentTheme) { 12222 currentTheme = WidgetPainter.visualTheme; 12223 auto ds = this.l.defaultStyle; 12224 if(auto ms = cast(MyTextStyle) ds) { 12225 auto cs = getComputedStyle(); 12226 auto font = cs.font(); 12227 if(font !is null) 12228 ms.font_ = font; 12229 else { 12230 auto osc = new OperatingSystemFont(); 12231 osc.loadDefault; 12232 ms.font_ = osc; 12233 } 12234 } 12235 } 12236 super.recomputeChildLayout(); 12237 } 12238 12239 private Point adjustForSingleLine(Point p) { 12240 if(singleLine) 12241 return Point(p.x, this.height / 2); 12242 else 12243 return p; 12244 } 12245 12246 private bool wordWrapEnabled_; 12247 12248 this(TextLayouter l, ScrollMessageWidget parent) { 12249 this.smw = parent; 12250 12251 smw.addDefaultWheelListeners(16, 16, 8); 12252 smw.movementPerButtonClick(16, 16); 12253 12254 this.defaultPadding = Rectangle(2, 2, 2, 2); 12255 12256 this.l = l; 12257 super(parent); 12258 12259 smw.addEventListener((scope ScrollEvent se) { 12260 this.redraw(); 12261 }); 12262 12263 bool mouseDown; 12264 12265 this.addEventListener((scope ResizeEvent re) { 12266 // FIXME: I should add a method to give this client area width thing 12267 if(wordWrapEnabled_) 12268 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12269 12270 adjustScrollbarSizes(); 12271 scrollForCaret(); 12272 12273 this.redraw(); 12274 }); 12275 12276 this.addEventListener((scope KeyDownEvent kde) { 12277 switch(kde.key) { 12278 case Key.Up, Key.Down, Key.Left, Key.Right: 12279 case Key.Home, Key.End: 12280 stateCheckpoint = true; 12281 bool setPosition = false; 12282 switch(kde.key) { 12283 case Key.Up: l.selection.moveUp(); break; 12284 case Key.Down: l.selection.moveDown(); break; 12285 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12286 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12287 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12288 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12289 default: assert(0); 12290 } 12291 12292 if(kde.shiftKey) 12293 l.selection.setFocus(); 12294 else 12295 l.selection.setAnchor(); 12296 if(setPosition) 12297 l.selection.setUserXCoordinate(); 12298 scrollForCaret(); 12299 redraw(); 12300 break; 12301 case Key.PageUp, Key.PageDown: 12302 // FIXME 12303 scrollForCaret(); 12304 break; 12305 case Key.Delete: 12306 if(l.selection.isEmpty()) { 12307 l.selection.setAnchor(); 12308 l.selection.moveRight(); 12309 l.selection.setFocus(); 12310 } 12311 deleteContentOfSelection(); 12312 adjustScrollbarSizes(); 12313 scrollForCaret(); 12314 break; 12315 case Key.Insert: 12316 break; 12317 case Key.A: 12318 if(kde.ctrlKey) 12319 selectAll(); 12320 break; 12321 case Key.F: 12322 // find 12323 break; 12324 case Key.Z: 12325 if(kde.ctrlKey) 12326 undo(); 12327 break; 12328 case Key.R: 12329 if(kde.ctrlKey) 12330 redo(); 12331 break; 12332 case Key.X: 12333 if(kde.ctrlKey) 12334 cut(); 12335 break; 12336 case Key.C: 12337 if(kde.ctrlKey) 12338 copy(); 12339 break; 12340 case Key.V: 12341 if(kde.ctrlKey) 12342 paste(); 12343 break; 12344 case Key.F1: 12345 with(l.selection()) { 12346 moveToStartOfLine(); 12347 setAnchor(); 12348 moveToEndOfLine(); 12349 moveToIncludeAdjacentEndOfLineMarker(); 12350 setFocus(); 12351 replaceContent(""); 12352 } 12353 12354 redraw(); 12355 break; 12356 /* 12357 case Key.F2: 12358 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12359 //(cast(MyTextStyle) old).font, 12360 font2, 12361 Color.red))); 12362 redraw(); 12363 break; 12364 */ 12365 case Key.Tab: 12366 // we process the char event, so don't want to change focus on it 12367 if(acceptsTabInput) 12368 kde.preventDefault(); 12369 break; 12370 default: 12371 } 12372 }); 12373 12374 Point downAt; 12375 12376 static if(UsingSimpledisplayX11) 12377 this.addEventListener((scope ClickEvent ce) { 12378 if(ce.button == MouseButton.middle) { 12379 parentWindow.win.getPrimarySelection((txt) { 12380 l.selection.replaceContent(txt); 12381 redraw(); 12382 }); 12383 } 12384 }); 12385 12386 this.addEventListener((scope MouseDownEvent ce) { 12387 if(ce.button == MouseButton.left) { 12388 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12389 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12390 l.selection.setAnchor(); 12391 mouseDown = true; 12392 parentWindow.captureMouse(this); 12393 this.redraw(); 12394 } else if(ce.button == MouseButton.right) { 12395 this.showContextMenu(ce.clientX, ce.clientY); 12396 } 12397 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12398 }); 12399 12400 Timer autoscrollTimer; 12401 int autoscrollDirection; 12402 int autoscrollAmount; 12403 12404 void autoscroll() { 12405 switch(autoscrollDirection) { 12406 case 0: smw.scrollUp(autoscrollAmount); break; 12407 case 1: smw.scrollDown(autoscrollAmount); break; 12408 case 2: smw.scrollLeft(autoscrollAmount); break; 12409 case 3: smw.scrollRight(autoscrollAmount); break; 12410 default: assert(0); 12411 } 12412 12413 this.redraw(); 12414 } 12415 12416 void setAutoscrollTimer(int direction, int amount) { 12417 if(autoscrollTimer is null) { 12418 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12419 } 12420 12421 autoscrollDirection = direction; 12422 autoscrollAmount = amount; 12423 } 12424 12425 void stopAutoscrollTimer() { 12426 if(autoscrollTimer !is null) { 12427 autoscrollTimer.dispose(); 12428 autoscrollTimer = null; 12429 } 12430 autoscrollAmount = 0; 12431 autoscrollDirection = 0; 12432 } 12433 12434 this.addEventListener((scope MouseMoveEvent ce) { 12435 if(mouseDown) { 12436 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12437 12438 // FIXME: when scrolling i actually do want a timer. 12439 // i also want a zone near the sides of the window where i can auto scroll 12440 12441 auto scrollMultiplier = scaleWithDpi(16); 12442 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12443 12444 if(!singleLine && movedTo.y < 4) { 12445 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12446 } else 12447 if(!singleLine && (movedTo.y + 6) > this.height) { 12448 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12449 } else 12450 if(movedTo.x < 4) { 12451 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12452 } else 12453 if((movedTo.x + 6) > this.width) { 12454 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12455 } else 12456 stopAutoscrollTimer(); 12457 12458 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12459 l.selection.setFocus(); 12460 this.redraw(); 12461 } 12462 }); 12463 12464 this.addEventListener((scope MouseUpEvent ce) { 12465 // FIXME: assert primary selection 12466 if(mouseDown && ce.button == MouseButton.left) { 12467 stateCheckpoint = true; 12468 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12469 //l.selection.setFocus(); 12470 mouseDown = false; 12471 parentWindow.releaseMouseCapture(); 12472 stopAutoscrollTimer(); 12473 this.redraw(); 12474 } 12475 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12476 }); 12477 12478 this.addEventListener((scope CharEvent ce) { 12479 if(readonly) 12480 return; 12481 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12482 return; // skip the ctrl+x characters we don't care about as plain text 12483 12484 if(singleLine && ce.character == '\n') 12485 return; 12486 if(!acceptsTabInput && ce.character == '\t') 12487 return; 12488 12489 doStateCheckpoint(); 12490 12491 char[4] buffer; 12492 import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web 12493 auto stride = encode(buffer, ce.character); 12494 l.selection.replaceContent(buffer[0 .. stride]); 12495 l.selection.setUserXCoordinate(); 12496 adjustScrollbarSizes(); 12497 scrollForCaret(); 12498 redraw(); 12499 }); 12500 } 12501 12502 static class Style : Widget.Style { 12503 override WidgetBackground background() { 12504 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12505 } 12506 12507 override Color foregroundColor() { 12508 return WidgetPainter.visualTheme.foregroundColor; 12509 } 12510 12511 override FrameStyle borderStyle() { 12512 return FrameStyle.sunk; 12513 } 12514 12515 override MouseCursor cursor() { 12516 return GenericCursor.Text; 12517 } 12518 } 12519 mixin OverrideStyle!Style; 12520 12521 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 12522 override int maxHeight() { 12523 if(singleLine) 12524 return minHeight; 12525 else 12526 return super.maxHeight(); 12527 } 12528 12529 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12530 painter.drawText(upperLeft, text); 12531 } 12532 12533 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12534 //painter.setFont(font); 12535 12536 auto cs = getComputedStyle(); 12537 auto defaultColor = cs.foregroundColor; 12538 12539 auto old = painter.setClipRectangle(bounds); 12540 scope(exit) painter.setClipRectangle(old); 12541 12542 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12543 //writeln("Segment: ", txt); 12544 assert(style !is null); 12545 12546 auto myStyle = cast(MyTextStyle) style; 12547 assert(myStyle !is null); 12548 12549 painter.setFont(myStyle.font); 12550 // defaultColor = myStyle.color; // FIXME: so wrong 12551 12552 if(info.selections && info.boundingBox.width > 0) { 12553 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12554 painter.fillColor = color; 12555 painter.outlineColor = color; 12556 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12557 painter.outlineColor = cs.selectionForegroundColor; 12558 //painter.fillColor = Color.white; 12559 } else { 12560 painter.outlineColor = defaultColor; 12561 } 12562 12563 if(this.isFocused) 12564 foreach(idx, caret; carets) { 12565 if(idx == 0) 12566 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12567 painter.drawLine( 12568 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12569 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12570 ); 12571 } 12572 12573 if(txt.stripInternal.length) { 12574 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12575 } 12576 12577 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 12578 return false; 12579 } else { 12580 return true; 12581 } 12582 }, Rectangle(smw.position(), bounds.size)); 12583 12584 /+ 12585 int place = 0; 12586 int y = 75; 12587 foreach(width; widths) { 12588 painter.fillColor = Color.red; 12589 painter.drawRectangle(Point(place, y), Size(width, 75)); 12590 //y += 15; 12591 place += width; 12592 } 12593 +/ 12594 12595 return bounds; 12596 } 12597 12598 static class MyTextStyle : TextStyle { 12599 OperatingSystemFont font_; 12600 this(OperatingSystemFont font, bool passwordMode = false) { 12601 this.font_ = font; 12602 } 12603 12604 override OperatingSystemFont font() { 12605 return font_; 12606 } 12607 } 12608 } 12609 12610 /+ 12611 version(use_new_text_system) 12612 class TextWidget : Widget { 12613 TextLayouter l; 12614 ScrollMessageWidget smw; 12615 TextDisplayHelper helper; 12616 this(TextLayouter l, Widget parent) { 12617 this.l = l; 12618 super(parent); 12619 12620 smw = new ScrollMessageWidget(this); 12621 //smw.horizontalScrollBar.hide; 12622 //smw.verticalScrollBar.hide; 12623 smw.addDefaultWheelListeners(16, 16, 8); 12624 smw.movementPerButtonClick(16, 16); 12625 helper = new TextDisplayHelper(l, smw); 12626 12627 // no need to do this here since there's gonna be a resize 12628 // event immediately before any drawing 12629 // smw.setTotalArea(l.width, l.height); 12630 smw.setViewableArea( 12631 this.width - this.paddingLeft - this.paddingRight, 12632 this.height - this.paddingTop - this.paddingBottom); 12633 12634 /+ 12635 writeln(l.width, "x", l.height); 12636 +/ 12637 } 12638 } 12639 +/ 12640 12641 12642 12643 12644 /+ 12645 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12646 +/ 12647 12648 /// Contains the implementation of text editing 12649 abstract class EditableTextWidget : EditableTextWidgetParent { 12650 this(Widget parent) { 12651 super(parent); 12652 12653 version(custom_widgets) 12654 setupCustomTextEditing(); 12655 } 12656 12657 private bool wordWrapEnabled_; 12658 void wordWrapEnabled(bool enabled) { 12659 version(win32_widgets) { 12660 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12661 } else version(custom_widgets) { 12662 wordWrapEnabled_ = enabled; 12663 version(use_new_text_system) 12664 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12665 } else static assert(false); 12666 } 12667 12668 override int minWidth() { return scaleWithDpi(16); } 12669 override int widthStretchiness() { return 7; } 12670 override int widthShrinkiness() { return 1; } 12671 12672 version(use_new_text_system) 12673 override int maxHeight() { return tdh.maxHeight; } 12674 12675 version(use_new_text_system) 12676 override void focus() { if(tdh) tdh.focus(); else super.focus(); } 12677 12678 void selectAll() { 12679 version(win32_widgets) 12680 SendMessage(hwnd, EM_SETSEL, 0, -1); 12681 else version(custom_widgets) { 12682 version(use_new_text_system) 12683 tdh.selectAll(); 12684 else 12685 textLayout.selectAll(); 12686 redraw(); 12687 } 12688 } 12689 12690 version(use_new_text_system) 12691 TextDisplayHelper tdh; 12692 12693 @property string content() { 12694 version(win32_widgets) { 12695 wchar[4096] bufferstack; 12696 wchar[] buffer; 12697 auto len = GetWindowTextLength(hwnd); 12698 if(len < bufferstack.length) 12699 buffer = bufferstack[0 .. len + 1]; 12700 else 12701 buffer = new wchar[](len + 1); 12702 12703 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12704 if(l >= 0) 12705 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12706 else 12707 return null; 12708 } else version(custom_widgets) { 12709 version(use_new_text_system) { 12710 return textLayout.getTextString(); 12711 } else 12712 return textLayout.getPlainText(); 12713 } else static assert(false); 12714 } 12715 @property void content(string s) { 12716 version(win32_widgets) { 12717 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12718 SetWindowTextW(hwnd, bfr.ptr); 12719 } else version(custom_widgets) { 12720 version(use_new_text_system) { 12721 selectAll(); 12722 textLayout.selection.replaceContent(s); 12723 12724 tdh.adjustScrollbarSizes(); 12725 // these don't seem to help 12726 // tdh.smw.setPosition(0, 0); 12727 // tdh.scrollForCaret(); 12728 12729 redraw(); 12730 } else { 12731 textLayout.clear(); 12732 textLayout.addText(s); 12733 12734 { 12735 // FIXME: it should be able to get this info easier 12736 auto painter = draw(); 12737 textLayout.redoLayout(painter); 12738 } 12739 auto cbb = textLayout.contentBoundingBox(); 12740 setContentSize(cbb.width, cbb.height); 12741 /* 12742 textLayout.addText(ForegroundColor.red, s); 12743 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12744 textLayout.addText(" is the best!"); 12745 */ 12746 redraw(); 12747 } 12748 } 12749 else static assert(false); 12750 } 12751 12752 void addText(string txt) { 12753 version(custom_widgets) { 12754 version(use_new_text_system) { 12755 textLayout.appendText(txt); 12756 tdh.adjustScrollbarSizes(); 12757 redraw(); 12758 } else { 12759 textLayout.addText(txt); 12760 12761 { 12762 // FIXME: it should be able to get this info easier 12763 auto painter = draw(); 12764 textLayout.redoLayout(painter); 12765 } 12766 auto cbb = textLayout.contentBoundingBox(); 12767 setContentSize(cbb.width, cbb.height); 12768 } 12769 } else version(win32_widgets) { 12770 // get the current selection 12771 DWORD StartPos, EndPos; 12772 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12773 12774 // move the caret to the end of the text 12775 int outLength = GetWindowTextLengthW(hwnd); 12776 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12777 12778 // insert the text at the new caret position 12779 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12780 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12781 12782 // restore the previous selection 12783 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12784 } else static assert(0); 12785 } 12786 12787 version(custom_widgets) 12788 version(trash_text) 12789 override void paintFrameAndBackground(WidgetPainter painter) { 12790 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12791 } 12792 12793 version(use_new_text_system) 12794 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12795 return new TextDisplayHelper(textLayout, smw); 12796 } 12797 12798 version(use_new_text_system) 12799 TextStyle defaultTextStyle() { 12800 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 12801 } 12802 12803 version(use_new_text_system) 12804 private OperatingSystemFont getUsedFont() { 12805 auto cs = getComputedStyle(); 12806 auto font = cs.font; 12807 if(font is null) { 12808 font = new OperatingSystemFont; 12809 font.loadDefault(); 12810 } 12811 return font; 12812 } 12813 12814 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 12815 else version(custom_widgets) { 12816 // FIXME 12817 version(use_new_text_system) { 12818 TextLayouter textLayout; 12819 12820 void setupCustomTextEditing() { 12821 textLayout = new TextLayouter(defaultTextStyle()); 12822 auto smw = new ScrollMessageWidget(this); 12823 if(!showingHorizontalScroll) 12824 smw.horizontalScrollBar.hide(); 12825 if(!showingVerticalScroll) 12826 smw.verticalScrollBar.hide(); 12827 this.tabStop = false; 12828 smw.tabStop = false; 12829 tdh = textDisplayHelperFactory(textLayout, smw); 12830 12831 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 12832 if(textLayout) { 12833 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 12834 // the dpi change can change the font, so this informs the layouter that it has changed too 12835 style.font_ = getUsedFont(); 12836 12837 // arsd.core.writeln(this.parentWindow.win.actualDpi); 12838 } 12839 } 12840 }); 12841 } 12842 12843 } else { 12844 12845 static if(SimpledisplayTimerAvailable) 12846 Timer caretTimer; 12847 etc.TextLayout textLayout; 12848 12849 void setupCustomTextEditing() { 12850 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12851 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12852 } 12853 12854 override void paint(WidgetPainter painter) { 12855 if(parentWindow.win.closed) return; 12856 12857 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12858 12859 /* 12860 painter.outlineColor = Color.white; 12861 painter.fillColor = Color.white; 12862 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12863 */ 12864 12865 painter.outlineColor = Color.black; 12866 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12867 12868 textLayout.caretShowingOnScreen = false; 12869 12870 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12871 } 12872 } 12873 12874 static class Style : Widget.Style { 12875 override FrameStyle borderStyle() { 12876 return FrameStyle.sunk; 12877 } 12878 override MouseCursor cursor() { 12879 return GenericCursor.Text; 12880 } 12881 } 12882 mixin OverrideStyle!Style; 12883 } 12884 else static assert(false); 12885 12886 version(trash_text) 12887 version(custom_widgets) 12888 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12889 super.defaultEventHandler_mousedown(ev); 12890 if(parentWindow.win.closed) return; 12891 if(ev.button == MouseButton.left) { 12892 if(textLayout.selectNone()) 12893 redraw(); 12894 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 12895 this.focus(); 12896 //this.parentWindow.win.grabInput(); 12897 } else if(ev.button == MouseButton.middle) { 12898 static if(UsingSimpledisplayX11) { 12899 getPrimarySelection(parentWindow.win, (in char[] txt) { 12900 textLayout.insert(txt); 12901 redraw(); 12902 12903 auto cbb = textLayout.contentBoundingBox(); 12904 setContentSize(cbb.width, cbb.height); 12905 }); 12906 } 12907 } 12908 } 12909 12910 version(trash_text) 12911 version(custom_widgets) 12912 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 12913 //this.parentWindow.win.releaseInputGrab(); 12914 super.defaultEventHandler_mouseup(ev); 12915 } 12916 12917 version(trash_text) 12918 version(custom_widgets) 12919 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 12920 super.defaultEventHandler_mousemove(ev); 12921 if(ev.state & ModifierState.leftButtonDown) { 12922 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 12923 redraw(); 12924 } 12925 } 12926 12927 version(trash_text) 12928 version(custom_widgets) 12929 override void defaultEventHandler_focus(Event ev) { 12930 super.defaultEventHandler_focus(ev); 12931 if(parentWindow.win.closed) return; 12932 auto painter = this.draw(); 12933 textLayout.drawCaret(painter); 12934 12935 static if(SimpledisplayTimerAvailable) 12936 if(caretTimer) { 12937 caretTimer.destroy(); 12938 caretTimer = null; 12939 } 12940 12941 bool blinkingCaret = true; 12942 static if(UsingSimpledisplayX11) 12943 if(!Image.impl.xshmAvailable) 12944 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 12945 12946 if(blinkingCaret) 12947 static if(SimpledisplayTimerAvailable) 12948 caretTimer = new Timer(500, { 12949 if(parentWindow.win.closed) { 12950 caretTimer.destroy(); 12951 return; 12952 } 12953 if(isFocused()) { 12954 auto painter = this.draw(); 12955 textLayout.drawCaret(painter); 12956 } else if(textLayout.caretShowingOnScreen) { 12957 auto painter = this.draw(); 12958 textLayout.eraseCaret(painter); 12959 } 12960 }); 12961 } 12962 12963 version(trash_text) { 12964 private string lastContentBlur; 12965 12966 override void defaultEventHandler_blur(Event ev) { 12967 super.defaultEventHandler_blur(ev); 12968 if(parentWindow.win.closed) return; 12969 version(custom_widgets) { 12970 auto painter = this.draw(); 12971 textLayout.eraseCaret(painter); 12972 static if(SimpledisplayTimerAvailable) 12973 if(caretTimer) { 12974 caretTimer.destroy(); 12975 caretTimer = null; 12976 } 12977 } 12978 12979 if(this.content != lastContentBlur) { 12980 auto evt = new ChangeEvent!string(this, &this.content); 12981 evt.dispatch(); 12982 lastContentBlur = this.content; 12983 } 12984 } 12985 } 12986 12987 version(win32_widgets) { 12988 private string lastContentBlur; 12989 12990 override void defaultEventHandler_blur(Event ev) { 12991 super.defaultEventHandler_blur(ev); 12992 12993 if(this.content != lastContentBlur) { 12994 auto evt = new ChangeEvent!string(this, &this.content); 12995 evt.dispatch(); 12996 lastContentBlur = this.content; 12997 } 12998 } 12999 } 13000 13001 13002 version(trash_text) 13003 version(custom_widgets) 13004 override void defaultEventHandler_char(CharEvent ev) { 13005 super.defaultEventHandler_char(ev); 13006 textLayout.insert(ev.character); 13007 redraw(); 13008 13009 // FIXME: too inefficient 13010 auto cbb = textLayout.contentBoundingBox(); 13011 setContentSize(cbb.width, cbb.height); 13012 } 13013 version(trash_text) 13014 version(custom_widgets) 13015 override void defaultEventHandler_keydown(KeyDownEvent ev) { 13016 //super.defaultEventHandler_keydown(ev); 13017 switch(ev.key) { 13018 case Key.Delete: 13019 textLayout.delete_(); 13020 redraw(); 13021 break; 13022 case Key.Left: 13023 textLayout.moveLeft(); 13024 redraw(); 13025 break; 13026 case Key.Right: 13027 textLayout.moveRight(); 13028 redraw(); 13029 break; 13030 case Key.Up: 13031 textLayout.moveUp(); 13032 redraw(); 13033 break; 13034 case Key.Down: 13035 textLayout.moveDown(); 13036 redraw(); 13037 break; 13038 case Key.Home: 13039 textLayout.moveHome(); 13040 redraw(); 13041 break; 13042 case Key.End: 13043 textLayout.moveEnd(); 13044 redraw(); 13045 break; 13046 case Key.PageUp: 13047 foreach(i; 0 .. 32) 13048 textLayout.moveUp(); 13049 redraw(); 13050 break; 13051 case Key.PageDown: 13052 foreach(i; 0 .. 32) 13053 textLayout.moveDown(); 13054 redraw(); 13055 break; 13056 13057 default: 13058 {} // intentionally blank, let "char" handle it 13059 } 13060 /* 13061 if(ev.key == Key.Backspace) { 13062 textLayout.backspace(); 13063 redraw(); 13064 } 13065 */ 13066 ensureVisibleInScroll(textLayout.caretBoundingBox()); 13067 } 13068 13069 version(use_new_text_system) { 13070 bool showingVerticalScroll() { return true; } 13071 bool showingHorizontalScroll() { return true; } 13072 } 13073 } 13074 13075 /// 13076 class LineEdit : EditableTextWidget { 13077 // FIXME: hack 13078 version(custom_widgets) { 13079 override bool showingVerticalScroll() { return false; } 13080 override bool showingHorizontalScroll() { return false; } 13081 } 13082 13083 override int flexBasisWidth() { return 250; } 13084 13085 /// 13086 this(Widget parent) { 13087 super(parent); 13088 version(win32_widgets) { 13089 createWin32Window(this, "edit"w, "", 13090 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13091 } else version(custom_widgets) { 13092 version(trash_text) { 13093 setupCustomTextEditing(); 13094 addEventListener(delegate(CharEvent ev) { 13095 if(ev.character == '\n') 13096 ev.preventDefault(); 13097 }); 13098 } 13099 } else static assert(false); 13100 } 13101 13102 version(use_new_text_system) 13103 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13104 auto tdh = new TextDisplayHelper(textLayout, smw); 13105 tdh.singleLine = true; 13106 return tdh; 13107 } 13108 13109 version(win32_widgets) { 13110 mixin Padding!q{0}; 13111 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13112 override int maxHeight() { return minHeight; } 13113 } 13114 13115 /+ 13116 @property void passwordMode(bool p) { 13117 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 13118 } 13119 +/ 13120 } 13121 13122 /++ 13123 A [LineEdit] that displays `*` in place of the actual characters. 13124 13125 Alas, Windows requires the window to be created differently to use this style, 13126 so it had to be a new class instead of a toggle on and off on an existing object. 13127 13128 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 13129 13130 History: 13131 Added January 24, 2021 13132 +/ 13133 class PasswordEdit : EditableTextWidget { 13134 version(custom_widgets) { 13135 override bool showingVerticalScroll() { return false; } 13136 override bool showingHorizontalScroll() { return false; } 13137 } 13138 13139 override int flexBasisWidth() { return 250; } 13140 13141 version(use_new_text_system) 13142 override TextStyle defaultTextStyle() { 13143 auto cs = getComputedStyle(); 13144 13145 auto osf = new class OperatingSystemFont { 13146 this() { 13147 super(cs.font); 13148 } 13149 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 13150 int count = 0; 13151 foreach(dchar ch; text) 13152 count++; 13153 return count * super.stringWidth("*", window); 13154 } 13155 }; 13156 13157 return new TextDisplayHelper.MyTextStyle(osf); 13158 } 13159 13160 version(use_new_text_system) 13161 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13162 static class TDH : TextDisplayHelper { 13163 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13164 singleLine = true; 13165 super(textLayout, smw); 13166 } 13167 13168 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13169 char[256] buffer = void; 13170 int bufferLength = 0; 13171 foreach(dchar ch; text) 13172 buffer[bufferLength++] = '*'; 13173 painter.drawText(upperLeft, buffer[0..bufferLength]); 13174 } 13175 } 13176 13177 return new TDH(textLayout, smw); 13178 } 13179 13180 /// 13181 this(Widget parent) { 13182 super(parent); 13183 version(win32_widgets) { 13184 createWin32Window(this, "edit"w, "", 13185 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13186 } else version(custom_widgets) { 13187 version(trash_text) 13188 setupCustomTextEditing(); 13189 addEventListener(delegate(CharEvent ev) { 13190 if(ev.character == '\n') 13191 ev.preventDefault(); 13192 }); 13193 } else static assert(false); 13194 } 13195 version(win32_widgets) { 13196 mixin Padding!q{2}; 13197 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13198 override int maxHeight() { return minHeight; } 13199 } 13200 } 13201 13202 /// 13203 class TextEdit : EditableTextWidget { 13204 /// 13205 this(Widget parent) { 13206 super(parent); 13207 version(win32_widgets) { 13208 createWin32Window(this, "edit"w, "", 13209 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 13210 } else version(custom_widgets) { 13211 version(trash_text) 13212 setupCustomTextEditing(); 13213 } else static assert(false); 13214 } 13215 override int maxHeight() { return int.max; } 13216 override int heightStretchiness() { return 7; } 13217 13218 override int flexBasisWidth() { return 250; } 13219 override int flexBasisHeight() { return 25; } 13220 } 13221 13222 13223 /+ 13224 /++ 13225 13226 +/ 13227 version(none) 13228 class RichTextDisplay : Widget { 13229 @property void content(string c) {} 13230 void appendContent(string c) {} 13231 } 13232 +/ 13233 13234 /++ 13235 A read-only text display 13236 13237 History: 13238 Added October 31, 2023 (dub v11.3) 13239 +/ 13240 class TextDisplay : EditableTextWidget { 13241 this(string text, Widget parent) { 13242 super(parent); 13243 this.content = text; 13244 } 13245 13246 override int maxHeight() { return int.max; } 13247 override int minHeight() { return 50; } 13248 override int heightStretchiness() { return 7; } 13249 13250 override int flexBasisWidth() { return 250; } 13251 override int flexBasisHeight() { return 50; } 13252 13253 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13254 return new MyTextDisplayHelper(textLayout, smw); 13255 } 13256 13257 override void registerMovement() { 13258 super.registerMovement(); 13259 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 13260 } 13261 13262 static class MyTextDisplayHelper : TextDisplayHelper { 13263 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13264 smw.verticalScrollBar.hide(); 13265 smw.horizontalScrollBar.hide(); 13266 super(textLayout, smw); 13267 this.readonly = true; 13268 } 13269 13270 override void registerMovement() { 13271 super.registerMovement(); 13272 13273 // FIXME: do the horizontal one too as needed and make sure that it does 13274 // wordwrapping again 13275 if(l.height + smw.horizontalScrollBar.height > this.height) 13276 smw.verticalScrollBar.show(); 13277 else 13278 smw.verticalScrollBar.hide(); 13279 13280 l.wordWrapWidth = this.width; 13281 13282 smw.verticalScrollBar.setPosition = 0; 13283 } 13284 13285 class Style : Widget.Style { 13286 // just want the generic look for these 13287 } 13288 13289 mixin OverrideStyle!Style; 13290 } 13291 } 13292 13293 /// 13294 class MessageBox : Window { 13295 private string message; 13296 MessageBoxButton buttonPressed = MessageBoxButton.None; 13297 /// 13298 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13299 super(300, 100); 13300 13301 assert(buttons.length); 13302 assert(buttons.length == buttonIds.length); 13303 13304 this.message = message; 13305 13306 auto label = new TextDisplay(message, this); 13307 13308 auto hl = new HorizontalLayout(this); 13309 auto spacer = new HorizontalSpacer(hl); // to right align 13310 13311 foreach(idx, buttonText; buttons) { 13312 auto button = new CommandButton(buttonText, hl); 13313 13314 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 13315 this.buttonPressed = buttonIds[idx]; 13316 win.close(); 13317 }; })(idx)); 13318 13319 if(idx == 0) 13320 button.focus(); 13321 } 13322 13323 if(buttons.length == 1) 13324 auto spacer2 = new HorizontalSpacer(hl); // to center it 13325 13326 win.resize(scaleWithDpi(300), this.minHeight()); 13327 13328 win.show(); 13329 redraw(); 13330 } 13331 13332 mixin Padding!q{16}; 13333 } 13334 13335 /// 13336 enum MessageBoxStyle { 13337 OK, /// 13338 OKCancel, /// 13339 RetryCancel, /// 13340 YesNo, /// 13341 YesNoCancel, /// 13342 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. 13343 } 13344 13345 /// 13346 enum MessageBoxIcon { 13347 None, /// 13348 Info, /// 13349 Warning, /// 13350 Error /// 13351 } 13352 13353 /// Identifies the button the user pressed on a message box. 13354 enum MessageBoxButton { 13355 None, /// The user closed the message box without clicking any of the buttons. 13356 OK, /// 13357 Cancel, /// 13358 Retry, /// 13359 Yes, /// 13360 No, /// 13361 Continue /// 13362 } 13363 13364 13365 /++ 13366 Displays a modal message box, blocking until the user dismisses it. 13367 13368 Returns: the button pressed. 13369 +/ 13370 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13371 version(win32_widgets) { 13372 WCharzBuffer t = WCharzBuffer(title); 13373 WCharzBuffer m = WCharzBuffer(message); 13374 UINT type; 13375 with(MessageBoxStyle) 13376 final switch(style) { 13377 case OK: type |= MB_OK; break; 13378 case OKCancel: type |= MB_OKCANCEL; break; 13379 case RetryCancel: type |= MB_RETRYCANCEL; break; 13380 case YesNo: type |= MB_YESNO; break; 13381 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13382 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13383 } 13384 with(MessageBoxIcon) 13385 final switch(icon) { 13386 case None: break; 13387 case Info: type |= MB_ICONINFORMATION; break; 13388 case Warning: type |= MB_ICONWARNING; break; 13389 case Error: type |= MB_ICONERROR; break; 13390 } 13391 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13392 case IDOK: return MessageBoxButton.OK; 13393 case IDCANCEL: return MessageBoxButton.Cancel; 13394 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13395 case IDYES: return MessageBoxButton.Yes; 13396 case IDNO: return MessageBoxButton.No; 13397 case IDCONTINUE: return MessageBoxButton.Continue; 13398 default: return MessageBoxButton.None; 13399 } 13400 } else { 13401 string[] buttons; 13402 MessageBoxButton[] buttonIds; 13403 with(MessageBoxStyle) 13404 final switch(style) { 13405 case OK: 13406 buttons = ["OK"]; 13407 buttonIds = [MessageBoxButton.OK]; 13408 break; 13409 case OKCancel: 13410 buttons = ["OK", "Cancel"]; 13411 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13412 break; 13413 case RetryCancel: 13414 buttons = ["Retry", "Cancel"]; 13415 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13416 break; 13417 case YesNo: 13418 buttons = ["Yes", "No"]; 13419 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13420 break; 13421 case YesNoCancel: 13422 buttons = ["Yes", "No", "Cancel"]; 13423 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13424 break; 13425 case RetryCancelContinue: 13426 buttons = ["Try Again", "Cancel", "Continue"]; 13427 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13428 break; 13429 } 13430 auto mb = new MessageBox(message, buttons, buttonIds); 13431 EventLoop el = EventLoop.get; 13432 el.run(() { return !mb.win.closed; }); 13433 return mb.buttonPressed; 13434 } 13435 } 13436 13437 /// ditto 13438 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13439 return messageBox(null, message, style, icon); 13440 } 13441 13442 13443 13444 /// 13445 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13446 13447 /++ 13448 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13449 13450 History: 13451 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. 13452 +/ 13453 struct EventListener { 13454 private Widget widget; 13455 private string event; 13456 private EventHandler handler; 13457 private bool useCapture; 13458 13459 /// 13460 void disconnect() { 13461 widget.removeEventListener(this); 13462 } 13463 } 13464 13465 /++ 13466 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13467 13468 Now, I recommend you use a statically typed event object instead. 13469 13470 See_Also: [Event] 13471 +/ 13472 enum EventType : string { 13473 click = "click", /// 13474 13475 mouseenter = "mouseenter", /// 13476 mouseleave = "mouseleave", /// 13477 mousein = "mousein", /// 13478 mouseout = "mouseout", /// 13479 mouseup = "mouseup", /// 13480 mousedown = "mousedown", /// 13481 mousemove = "mousemove", /// 13482 13483 keydown = "keydown", /// 13484 keyup = "keyup", /// 13485 char_ = "char", /// 13486 13487 focus = "focus", /// 13488 blur = "blur", /// 13489 13490 triggered = "triggered", /// 13491 13492 change = "change", /// 13493 } 13494 13495 /++ 13496 Represents an event that is currently being processed. 13497 13498 13499 Minigui's event model is based on the web browser. An event has a name, a target, 13500 and an associated data object. It starts from the window and works its way down through 13501 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13502 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13503 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13504 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13505 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13506 whenever propagation is done, not only if it gets to the end of the chain). 13507 13508 This model has several nice points: 13509 13510 $(LIST 13511 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13512 with event handlers set, then add/remove children as much as you want without needing 13513 to manage the event handlers on them - the parent alone can manage everything. 13514 13515 * It is easy to create new custom events in your application. 13516 13517 * It is familiar to many web developers. 13518 ) 13519 13520 There's a few downsides though: 13521 13522 $(LIST 13523 * There's not a lot of type safety. 13524 13525 * You don't get a static list of what events a widget can emit. 13526 13527 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13528 the central delegation benefit is it can be lead to debugging of action at a distance. 13529 ) 13530 13531 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13532 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13533 to simply use a D object type which provides a static interface as well as a built-in event name. 13534 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13535 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13536 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13537 to having a little more help from the D compiler and documentation generator. 13538 13539 Your code would change like this: 13540 13541 --- 13542 // old 13543 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13544 13545 // new 13546 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13547 --- 13548 13549 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. 13550 13551 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13552 13553 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13554 13555 Thus the family of functions are: 13556 13557 [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. 13558 13559 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13560 13561 [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. 13562 13563 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13564 13565 --- 13566 class MyCheckbox : Widget { 13567 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13568 /// It is NOT actually required but should be used whenever possible. 13569 mixin Emits!(ChangeEvent!bool); 13570 13571 this(Widget parent) { 13572 super(parent); 13573 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13574 } 13575 13576 private bool _checked; 13577 @property bool checked() { return _checked; } 13578 @property void checked(bool set) { 13579 _checked = set; 13580 emit!(ChangeEvent!bool)(&checked); 13581 } 13582 } 13583 --- 13584 13585 ## Creating Your Own Events 13586 13587 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. 13588 13589 --- 13590 class MyEvent : Event { 13591 this(Widget target) { super(EventString, target); } 13592 mixin Register; // adds EventString and other reflection information 13593 } 13594 --- 13595 13596 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13597 13598 History: 13599 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. 13600 13601 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. 13602 +/ 13603 /+ 13604 13605 ## General Conventions 13606 13607 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. 13608 13609 13610 ## Qt-style signals and slots 13611 13612 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. 13613 13614 The intention is for events to be used when 13615 13616 --- 13617 class Demo : Widget { 13618 this() { 13619 myPropertyChanged = Signal!int(this); 13620 } 13621 @property myProperty(int v) { 13622 myPropertyChanged.emit(v); 13623 } 13624 13625 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 13626 // but it can just genuinely not care about `this` since that's not really passed. 13627 } 13628 13629 class Foo : Widget { 13630 // the slot uda is not necessary, but it helps the script and ui builder find it. 13631 @slot void setValue(int v) { ... } 13632 } 13633 13634 demo.myPropertyChanged.connect(&foo.setValue); 13635 --- 13636 13637 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 13638 13639 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 13640 13641 class StringChangeEvent : ChangeEvent, Signal!string { 13642 mixin SignalImpl 13643 } 13644 13645 +/ 13646 class Event : ReflectableProperties { 13647 /// Creates an event without populating any members and without sending it. See [dispatch] 13648 this(string eventName, Widget emittedBy) { 13649 this.eventName = eventName; 13650 this.srcElement = emittedBy; 13651 } 13652 13653 13654 /// Implementations for the [ReflectableProperties] interface/ 13655 void getPropertiesList(scope void delegate(string name) sink) const {} 13656 /// ditto 13657 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 13658 /// ditto 13659 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 13660 return SetPropertyResult.notPermitted; 13661 } 13662 13663 13664 /+ 13665 /++ 13666 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 13667 13668 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. 13669 +/ 13670 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 13671 if(value.length == 0) { 13672 finalSink(memberName, `""`); 13673 return; 13674 } 13675 13676 char[1024] bufferBacking; 13677 char[] buffer = bufferBacking; 13678 int bufferPosition; 13679 13680 void sink(char ch) { 13681 if(bufferPosition >= buffer.length) 13682 buffer.length = buffer.length + 1024; 13683 buffer[bufferPosition++] = ch; 13684 } 13685 13686 sink('"'); 13687 13688 foreach(ch; value) { 13689 switch(ch) { 13690 case '\\': 13691 sink('\\'); sink('\\'); 13692 break; 13693 case '"': 13694 sink('\\'); sink('"'); 13695 break; 13696 case '\n': 13697 sink('\\'); sink('n'); 13698 break; 13699 case '\r': 13700 sink('\\'); sink('r'); 13701 break; 13702 case '\t': 13703 sink('\\'); sink('t'); 13704 break; 13705 default: 13706 sink(ch); 13707 } 13708 } 13709 13710 sink('"'); 13711 13712 finalSink(memberName, buffer[0 .. bufferPosition]); 13713 } 13714 +/ 13715 13716 /+ 13717 enum EventInitiator { 13718 system, 13719 minigui, 13720 user 13721 } 13722 13723 immutable EventInitiator; initiatedBy; 13724 +/ 13725 13726 /++ 13727 Events should generally follow the propagation model, but there's some exceptions 13728 to that rule. If so, they should override this to return false. In that case, only 13729 bubbling event handlers on the target itself and capturing event handlers on the containing 13730 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 13731 capture -> target -> bubble process.) 13732 13733 History: 13734 Added May 12, 2021 13735 +/ 13736 bool propagates() const pure nothrow @nogc @safe { 13737 return true; 13738 } 13739 13740 /++ 13741 hints as to whether preventDefault will actually do anything. not entirely reliable. 13742 13743 History: 13744 Added May 14, 2021 13745 +/ 13746 bool cancelable() const pure nothrow @nogc @safe { 13747 return true; 13748 } 13749 13750 /++ 13751 You can mix this into child class to register some boilerplate. It includes the `EventString` 13752 member, a constructor, and implementations of the dynamic get data interfaces. 13753 13754 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 13755 13756 13757 You can override the default EventString by simply providing your own in the form of 13758 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 13759 which provides some namespace protection against conflicts in other libraries while still being fairly 13760 easy to use. 13761 13762 If you provide your own constructor, it will override the default constructor provided here. A constructor 13763 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 13764 first argument to your constructor. 13765 13766 History: 13767 Added May 13, 2021. 13768 +/ 13769 protected static mixin template Register() { 13770 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 13771 this(Widget target) { super(EventString, target); } 13772 13773 mixin ReflectableProperties.RegisterGetters; 13774 } 13775 13776 /++ 13777 This is the widget that emitted the event. 13778 13779 13780 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 13781 13782 History: 13783 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 13784 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 13785 so I don't intend to remove these aliases. 13786 +/ 13787 Widget source; 13788 /// ditto 13789 alias source target; 13790 /// ditto 13791 alias source srcElement; 13792 13793 Widget relatedTarget; /// Note: likely to be deprecated at some point. 13794 13795 /// Prevents the default event handler (if there is one) from being called 13796 void preventDefault() { 13797 lastDefaultPrevented = true; 13798 defaultPrevented = true; 13799 } 13800 13801 /// Stops the event propagation immediately. 13802 void stopPropagation() { 13803 propagationStopped = true; 13804 } 13805 13806 private bool defaultPrevented; 13807 private bool propagationStopped; 13808 private string eventName; 13809 13810 private bool isBubbling; 13811 13812 /// 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. 13813 protected void adjustScrolling() { } 13814 /// ditto 13815 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 13816 13817 /++ 13818 this sends it only to the target. If you want propagation, use dispatch() instead. 13819 13820 This should be made private!!! 13821 13822 +/ 13823 void sendDirectly() { 13824 if(srcElement is null) 13825 return; 13826 13827 // 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. 13828 13829 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13830 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 13831 13832 adjustScrolling(); 13833 13834 if(auto e = target.parentWindow) { 13835 if(auto handlers = "*" in e.capturingEventHandlers) 13836 foreach(handler; *handlers) 13837 if(handler) handler(e, this); 13838 if(auto handlers = eventName in e.capturingEventHandlers) 13839 foreach(handler; *handlers) 13840 if(handler) handler(e, this); 13841 } 13842 13843 auto e = srcElement; 13844 13845 if(auto handlers = eventName in e.bubblingEventHandlers) 13846 foreach(handler; *handlers) 13847 if(handler) handler(e, this); 13848 13849 if(auto handlers = "*" in e.bubblingEventHandlers) 13850 foreach(handler; *handlers) 13851 if(handler) handler(e, this); 13852 13853 // there's never a default for a catch-all event 13854 if(!defaultPrevented) 13855 if(eventName in e.defaultEventHandlers) 13856 e.defaultEventHandlers[eventName](e, this); 13857 } 13858 13859 /// this dispatches the element using the capture -> target -> bubble process 13860 void dispatch() { 13861 if(srcElement is null) 13862 return; 13863 13864 if(!propagates) { 13865 sendDirectly; 13866 return; 13867 } 13868 13869 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13870 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 13871 13872 adjustScrolling(); 13873 // first capture, then bubble 13874 13875 Widget[] chain; 13876 Widget curr = srcElement; 13877 while(curr) { 13878 auto l = curr; 13879 chain ~= l; 13880 curr = curr.parent; 13881 } 13882 13883 isBubbling = false; 13884 13885 foreach_reverse(e; chain) { 13886 if(auto handlers = "*" in e.capturingEventHandlers) 13887 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13888 13889 if(propagationStopped) 13890 break; 13891 13892 if(auto handlers = eventName in e.capturingEventHandlers) 13893 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13894 13895 // the default on capture should really be to always do nothing 13896 13897 //if(!defaultPrevented) 13898 // if(eventName in e.defaultEventHandlers) 13899 // e.defaultEventHandlers[eventName](e.element, this); 13900 13901 if(propagationStopped) 13902 break; 13903 } 13904 13905 int adjustX; 13906 int adjustY; 13907 13908 isBubbling = true; 13909 if(!propagationStopped) 13910 foreach(e; chain) { 13911 if(auto handlers = eventName in e.bubblingEventHandlers) 13912 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13913 13914 if(propagationStopped) 13915 break; 13916 13917 if(auto handlers = "*" in e.bubblingEventHandlers) 13918 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13919 13920 if(propagationStopped) 13921 break; 13922 13923 if(e.encapsulatedChildren()) { 13924 adjustClientCoordinates(adjustX, adjustY); 13925 target = e; 13926 } else { 13927 adjustX += e.x; 13928 adjustY += e.y; 13929 } 13930 } 13931 13932 if(!defaultPrevented) 13933 foreach(e; chain) { 13934 if(eventName in e.defaultEventHandlers) 13935 e.defaultEventHandlers[eventName](e, this); 13936 } 13937 } 13938 13939 13940 /* old compatibility things */ 13941 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 13942 final @property { 13943 Key key() { return (cast(KeyEventBase) this).key; } 13944 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 13945 13946 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 13947 bool altKey() { return (cast(KeyEventBase) this).altKey; } 13948 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 13949 } 13950 13951 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 13952 final @property { 13953 int clientX() { return (cast(MouseEventBase) this).clientX; } 13954 int clientY() { return (cast(MouseEventBase) this).clientY; } 13955 13956 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 13957 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 13958 13959 int button() { return (cast(MouseEventBase) this).button; } 13960 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 13961 } 13962 13963 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 13964 final @property { 13965 int state() { 13966 if(auto meb = cast(MouseEventBase) this) 13967 return meb.state; 13968 if(auto keb = cast(KeyEventBase) this) 13969 return keb.state; 13970 assert(0); 13971 } 13972 } 13973 13974 deprecated("Use a CharEvent instead of Event in your handler going forward") 13975 final @property { 13976 dchar character() { 13977 if(auto ce = cast(CharEvent) this) 13978 return ce.character; 13979 return dchar.init; 13980 } 13981 } 13982 13983 // for change events 13984 @property { 13985 /// 13986 int intValue() { return 0; } 13987 /// 13988 string stringValue() { return null; } 13989 } 13990 } 13991 13992 /++ 13993 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 13994 13995 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 13996 dynamic and custom events, but the static list helps ensure you get them right. 13997 13998 If this is declared, you can use [Widget.emit] to send the event. 13999 14000 All events work the same way though, following the capture->widget->bubble model described under [Event]. 14001 14002 History: 14003 Added May 4, 2021 14004 +/ 14005 mixin template Emits(EventType) { 14006 import arsd.minigui : EventString; 14007 static if(is(EventType : Event) && !is(EventType == Event)) 14008 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 14009 else 14010 static assert(0, "You can only emit subclasses of Event"); 14011 } 14012 14013 /// ditto 14014 mixin template Emits(string eventString) { 14015 mixin("private Event[0] emits_" ~ eventString ~";"); 14016 } 14017 14018 /* 14019 class SignalEvent(string name) : Event { 14020 14021 } 14022 */ 14023 14024 /++ 14025 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". 14026 14027 14028 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. 14029 14030 History: 14031 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 14032 +/ 14033 class CommandEvent : Event { 14034 enum EventString = "command"; 14035 this(Widget source, string CommandString = EventString) { 14036 super(CommandString, source); 14037 } 14038 } 14039 14040 /++ 14041 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 14042 +/ 14043 class CommandEventWithArgs(Args...) : CommandEvent { 14044 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 14045 Args args; 14046 } 14047 14048 /++ 14049 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. 14050 14051 See [CommandEvent] for more information. 14052 14053 Returns: 14054 The [EventListener] you can use to remove the handler. 14055 +/ 14056 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 14057 return w.addEventListener(CommandString, (Event ev) { 14058 if(ev.target is w) 14059 return; // it does not consume its own commands! 14060 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 14061 handler(cev.args); 14062 ev.stopPropagation(); 14063 } 14064 }); 14065 } 14066 14067 /++ 14068 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. 14069 +/ 14070 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 14071 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 14072 event.dispatch(); 14073 } 14074 14075 class ResizeEvent : Event { 14076 enum EventString = "resize"; 14077 14078 this(Widget target) { super(EventString, target); } 14079 14080 override bool propagates() const { return false; } 14081 } 14082 14083 /++ 14084 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 14085 14086 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. 14087 14088 History: 14089 Added June 21, 2021 (dub v10.1) 14090 +/ 14091 class ClosingEvent : Event { 14092 enum EventString = "closing"; 14093 14094 this(Widget target) { super(EventString, target); } 14095 14096 override bool propagates() const { return false; } 14097 override bool cancelable() const { return true; } 14098 } 14099 14100 /// ditto 14101 class ClosedEvent : Event { 14102 enum EventString = "closed"; 14103 14104 this(Widget target) { super(EventString, target); } 14105 14106 override bool propagates() const { return false; } 14107 override bool cancelable() const { return false; } 14108 } 14109 14110 /// 14111 class BlurEvent : Event { 14112 enum EventString = "blur"; 14113 14114 // FIXME: related target? 14115 this(Widget target) { super(EventString, target); } 14116 14117 override bool propagates() const { return false; } 14118 } 14119 14120 /// 14121 class FocusEvent : Event { 14122 enum EventString = "focus"; 14123 14124 // FIXME: related target? 14125 this(Widget target) { super(EventString, target); } 14126 14127 override bool propagates() const { return false; } 14128 } 14129 14130 /++ 14131 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 14132 14133 History: 14134 Added July 3, 2021 14135 +/ 14136 class FocusInEvent : Event { 14137 enum EventString = "focusin"; 14138 14139 // FIXME: related target? 14140 this(Widget target) { super(EventString, target); } 14141 14142 override bool cancelable() const { return false; } 14143 } 14144 14145 /// ditto 14146 class FocusOutEvent : Event { 14147 enum EventString = "focusout"; 14148 14149 // FIXME: related target? 14150 this(Widget target) { super(EventString, target); } 14151 14152 override bool cancelable() const { return false; } 14153 } 14154 14155 /// 14156 class ScrollEvent : Event { 14157 enum EventString = "scroll"; 14158 this(Widget target) { super(EventString, target); } 14159 14160 override bool cancelable() const { return false; } 14161 } 14162 14163 /++ 14164 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 14165 14166 History: 14167 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 14168 +/ 14169 class CharEvent : Event { 14170 enum EventString = "char"; 14171 this(Widget target, dchar ch) { 14172 character = ch; 14173 super(EventString, target); 14174 } 14175 14176 immutable dchar character; 14177 } 14178 14179 /++ 14180 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 14181 +/ 14182 abstract class ChangeEventBase : Event { 14183 enum EventString = "change"; 14184 this(Widget target) { 14185 super(EventString, target); 14186 } 14187 14188 /+ 14189 // idk where or how exactly i want to do this. 14190 // i might come back to it later. 14191 14192 // If a widget itself broadcasts one of theses itself, it stops propagation going down 14193 // this way the source doesn't get too confused (think of a nested scroll widget) 14194 // 14195 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 14196 // then you consume that command and change you scroll x position to whatever. then you do 14197 // some kind of change event that is broadcast back to the children and any horizontal scroll 14198 // listeners are now able to update, without having an explicit connection between them. 14199 void broadcastToChildren(string fieldName) { 14200 14201 } 14202 +/ 14203 } 14204 14205 /++ 14206 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. 14207 14208 14209 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). 14210 14211 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);` 14212 14213 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 14214 14215 History: 14216 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. 14217 +/ 14218 class ChangeEvent(T) : ChangeEventBase { 14219 this(Widget target, T delegate() getNewValue) { 14220 assert(getNewValue !is null); 14221 this.getNewValue = getNewValue; 14222 super(target); 14223 } 14224 14225 private T delegate() getNewValue; 14226 14227 /++ 14228 Gets the new value that just changed. 14229 +/ 14230 @property T value() { 14231 return getNewValue(); 14232 } 14233 14234 /// compatibility method for old generic Events 14235 static if(is(immutable T == immutable int)) 14236 override int intValue() { return value; } 14237 /// ditto 14238 static if(is(immutable T == immutable string)) 14239 override string stringValue() { return value; } 14240 } 14241 14242 /++ 14243 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 14244 14245 14246 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14247 14248 History: 14249 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14250 +/ 14251 abstract class KeyEventBase : Event { 14252 this(string name, Widget target) { 14253 super(name, target); 14254 } 14255 14256 // for key events 14257 Key key; /// 14258 14259 KeyEvent originalKeyEvent; 14260 14261 /++ 14262 Indicates the current state of the given keyboard modifier keys. 14263 14264 History: 14265 Added to events on April 15, 2020. 14266 +/ 14267 bool ctrlKey; 14268 14269 /// ditto 14270 bool altKey; 14271 14272 /// ditto 14273 bool shiftKey; 14274 14275 /++ 14276 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 14277 14278 See [arsd.simpledisplay.ModifierState] for other possible flags. 14279 +/ 14280 int state; 14281 14282 mixin Register; 14283 } 14284 14285 /++ 14286 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]. 14287 14288 14289 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14290 14291 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. 14292 14293 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. 14294 14295 See_Also: [KeyUpEvent], [CharEvent] 14296 14297 History: 14298 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 14299 +/ 14300 class KeyDownEvent : KeyEventBase { 14301 enum EventString = "keydown"; 14302 this(Widget target) { super(EventString, target); } 14303 } 14304 14305 /++ 14306 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 14307 14308 14309 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14310 14311 See_Also: [KeyDownEvent], [CharEvent] 14312 14313 History: 14314 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 14315 +/ 14316 class KeyUpEvent : KeyEventBase { 14317 enum EventString = "keyup"; 14318 this(Widget target) { super(EventString, target); } 14319 } 14320 14321 /++ 14322 Contains shared properties for various mouse events; 14323 14324 14325 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14326 14327 History: 14328 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14329 +/ 14330 abstract class MouseEventBase : Event { 14331 this(string name, Widget target) { 14332 super(name, target); 14333 } 14334 14335 // for mouse events 14336 int clientX; /// The mouse event location relative to the target widget 14337 int clientY; /// ditto 14338 14339 int viewportX; /// The mouse event location relative to the window origin 14340 int viewportY; /// ditto 14341 14342 int button; /// See: [MouseEvent.button] 14343 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14344 14345 /++ 14346 Indicates the current state of the given keyboard modifier keys. 14347 14348 History: 14349 Added to mouse events on September 28, 2010. 14350 +/ 14351 bool ctrlKey; 14352 14353 /// ditto 14354 bool altKey; 14355 14356 /// ditto 14357 bool shiftKey; 14358 14359 14360 14361 int state; /// 14362 14363 /++ 14364 for consistent names with key event. 14365 14366 History: 14367 Added September 28, 2021 (dub v10.3) 14368 +/ 14369 alias modifierState = state; 14370 14371 /++ 14372 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14373 14374 History: 14375 Added May 15, 2021 14376 +/ 14377 bool isMouseWheel() { 14378 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14379 } 14380 14381 // private 14382 override void adjustClientCoordinates(int deltaX, int deltaY) { 14383 clientX += deltaX; 14384 clientY += deltaY; 14385 } 14386 14387 override void adjustScrolling() { 14388 version(custom_widgets) { // TEMP 14389 viewportX = clientX; 14390 viewportY = clientY; 14391 if(auto se = cast(ScrollableWidget) srcElement) { 14392 clientX += se.scrollOrigin.x; 14393 clientY += se.scrollOrigin.y; 14394 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14395 //clientX += se.scrollX_; 14396 //clientY += se.scrollY_; 14397 } 14398 } 14399 } 14400 14401 mixin Register; 14402 } 14403 14404 /++ 14405 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14406 14407 14408 $(WARNING 14409 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14410 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14411 behavior. 14412 ) 14413 14414 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14415 14416 [MouseUpEvent] is sent when the user releases a mouse button. 14417 14418 [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.) 14419 14420 [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. 14421 14422 [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. 14423 14424 [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. 14425 14426 [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. 14427 14428 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14429 14430 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14431 14432 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14433 14434 Rationale: 14435 14436 If you only want to do drag, mousedown/up works just fine being consistently sent. 14437 14438 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). 14439 14440 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. 14441 14442 History: 14443 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. 14444 +/ 14445 class MouseUpEvent : MouseEventBase { 14446 enum EventString = "mouseup"; /// 14447 this(Widget target) { super(EventString, target); } 14448 } 14449 /// ditto 14450 class MouseDownEvent : MouseEventBase { 14451 enum EventString = "mousedown"; /// 14452 this(Widget target) { super(EventString, target); } 14453 } 14454 /// ditto 14455 class MouseMoveEvent : MouseEventBase { 14456 enum EventString = "mousemove"; /// 14457 this(Widget target) { super(EventString, target); } 14458 } 14459 /// ditto 14460 class ClickEvent : MouseEventBase { 14461 enum EventString = "click"; /// 14462 this(Widget target) { super(EventString, target); } 14463 } 14464 /// ditto 14465 class DoubleClickEvent : MouseEventBase { 14466 enum EventString = "dblclick"; /// 14467 this(Widget target) { super(EventString, target); } 14468 } 14469 /// ditto 14470 class MouseOverEvent : Event { 14471 enum EventString = "mouseover"; /// 14472 this(Widget target) { super(EventString, target); } 14473 } 14474 /// ditto 14475 class MouseOutEvent : Event { 14476 enum EventString = "mouseout"; /// 14477 this(Widget target) { super(EventString, target); } 14478 } 14479 /// ditto 14480 class MouseEnterEvent : Event { 14481 enum EventString = "mouseenter"; /// 14482 this(Widget target) { super(EventString, target); } 14483 14484 override bool propagates() const { return false; } 14485 } 14486 /// ditto 14487 class MouseLeaveEvent : Event { 14488 enum EventString = "mouseleave"; /// 14489 this(Widget target) { super(EventString, target); } 14490 14491 override bool propagates() const { return false; } 14492 } 14493 14494 private bool isAParentOf(Widget a, Widget b) { 14495 if(a is null || b is null) 14496 return false; 14497 14498 while(b !is null) { 14499 if(a is b) 14500 return true; 14501 b = b.parent; 14502 } 14503 14504 return false; 14505 } 14506 14507 private struct WidgetAtPointResponse { 14508 Widget widget; 14509 14510 // x, y relative to the widget in the response. 14511 int x; 14512 int y; 14513 } 14514 14515 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14516 assert(starting !is null); 14517 14518 starting.addScrollPosition(x, y); 14519 14520 auto child = starting.getChildAtPosition(x, y); 14521 while(child) { 14522 if(child.hidden) 14523 continue; 14524 starting = child; 14525 x -= child.x; 14526 y -= child.y; 14527 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14528 child = r.widget; 14529 if(child is starting) 14530 break; 14531 } 14532 return WidgetAtPointResponse(starting, x, y); 14533 } 14534 14535 version(win32_widgets) { 14536 private: 14537 import core.sys.windows.commctrl; 14538 14539 pragma(lib, "comctl32"); 14540 shared static this() { 14541 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14542 INITCOMMONCONTROLSEX ic; 14543 ic.dwSize = cast(DWORD) ic.sizeof; 14544 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14545 if(!InitCommonControlsEx(&ic)) { 14546 //writeln("ICC failed"); 14547 } 14548 } 14549 14550 14551 // everything from here is just win32 headers copy pasta 14552 private: 14553 extern(Windows): 14554 14555 alias HANDLE HMENU; 14556 HMENU CreateMenu(); 14557 bool SetMenu(HWND, HMENU); 14558 HMENU CreatePopupMenu(); 14559 enum MF_POPUP = 0x10; 14560 enum MF_STRING = 0; 14561 14562 14563 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14564 struct INITCOMMONCONTROLSEX { 14565 DWORD dwSize; 14566 DWORD dwICC; 14567 } 14568 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14569 enum { 14570 IDB_STD_SMALL_COLOR, 14571 IDB_STD_LARGE_COLOR, 14572 IDB_VIEW_SMALL_COLOR = 4, 14573 IDB_VIEW_LARGE_COLOR = 5 14574 } 14575 enum { 14576 STD_CUT, 14577 STD_COPY, 14578 STD_PASTE, 14579 STD_UNDO, 14580 STD_REDOW, 14581 STD_DELETE, 14582 STD_FILENEW, 14583 STD_FILEOPEN, 14584 STD_FILESAVE, 14585 STD_PRINTPRE, 14586 STD_PROPERTIES, 14587 STD_HELP, 14588 STD_FIND, 14589 STD_REPLACE, 14590 STD_PRINT // = 14 14591 } 14592 14593 alias HANDLE HIMAGELIST; 14594 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14595 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14596 BOOL ImageList_Destroy(HIMAGELIST); 14597 14598 uint MAKELONG(ushort a, ushort b) { 14599 return cast(uint) ((b << 16) | a); 14600 } 14601 14602 14603 struct TBBUTTON { 14604 int iBitmap; 14605 int idCommand; 14606 BYTE fsState; 14607 BYTE fsStyle; 14608 version(Win64) 14609 BYTE[6] bReserved; 14610 else 14611 BYTE[2] bReserved; 14612 DWORD dwData; 14613 INT_PTR iString; 14614 } 14615 14616 enum { 14617 TB_ADDBUTTONSA = WM_USER + 20, 14618 TB_INSERTBUTTONA = WM_USER + 21, 14619 TB_GETIDEALSIZE = WM_USER + 99, 14620 } 14621 14622 struct SIZE { 14623 LONG cx; 14624 LONG cy; 14625 } 14626 14627 14628 enum { 14629 TBSTATE_CHECKED = 1, 14630 TBSTATE_PRESSED = 2, 14631 TBSTATE_ENABLED = 4, 14632 TBSTATE_HIDDEN = 8, 14633 TBSTATE_INDETERMINATE = 16, 14634 TBSTATE_WRAP = 32 14635 } 14636 14637 14638 14639 enum { 14640 ILC_COLOR = 0, 14641 ILC_COLOR4 = 4, 14642 ILC_COLOR8 = 8, 14643 ILC_COLOR16 = 16, 14644 ILC_COLOR24 = 24, 14645 ILC_COLOR32 = 32, 14646 ILC_COLORDDB = 254, 14647 ILC_MASK = 1, 14648 ILC_PALETTE = 2048 14649 } 14650 14651 14652 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 14653 14654 14655 enum { 14656 TB_ENABLEBUTTON = WM_USER + 1, 14657 TB_CHECKBUTTON, 14658 TB_PRESSBUTTON, 14659 TB_HIDEBUTTON, 14660 TB_INDETERMINATE, // = WM_USER + 5, 14661 TB_ISBUTTONENABLED = WM_USER + 9, 14662 TB_ISBUTTONCHECKED, 14663 TB_ISBUTTONPRESSED, 14664 TB_ISBUTTONHIDDEN, 14665 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 14666 TB_SETSTATE = WM_USER + 17, 14667 TB_GETSTATE = WM_USER + 18, 14668 TB_ADDBITMAP = WM_USER + 19, 14669 TB_DELETEBUTTON = WM_USER + 22, 14670 TB_GETBUTTON, 14671 TB_BUTTONCOUNT, 14672 TB_COMMANDTOINDEX, 14673 TB_SAVERESTOREA, 14674 TB_CUSTOMIZE, 14675 TB_ADDSTRINGA, 14676 TB_GETITEMRECT, 14677 TB_BUTTONSTRUCTSIZE, 14678 TB_SETBUTTONSIZE, 14679 TB_SETBITMAPSIZE, 14680 TB_AUTOSIZE, // = WM_USER + 33, 14681 TB_GETTOOLTIPS = WM_USER + 35, 14682 TB_SETTOOLTIPS = WM_USER + 36, 14683 TB_SETPARENT = WM_USER + 37, 14684 TB_SETROWS = WM_USER + 39, 14685 TB_GETROWS, 14686 TB_GETBITMAPFLAGS, 14687 TB_SETCMDID, 14688 TB_CHANGEBITMAP, 14689 TB_GETBITMAP, 14690 TB_GETBUTTONTEXTA, 14691 TB_REPLACEBITMAP, // = WM_USER + 46, 14692 TB_GETBUTTONSIZE = WM_USER + 58, 14693 TB_SETBUTTONWIDTH = WM_USER + 59, 14694 TB_GETBUTTONTEXTW = WM_USER + 75, 14695 TB_SAVERESTOREW = WM_USER + 76, 14696 TB_ADDSTRINGW = WM_USER + 77, 14697 } 14698 14699 extern(Windows) 14700 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 14701 14702 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 14703 14704 14705 enum { 14706 TB_SETINDENT = WM_USER + 47, 14707 TB_SETIMAGELIST, 14708 TB_GETIMAGELIST, 14709 TB_LOADIMAGES, 14710 TB_GETRECT, 14711 TB_SETHOTIMAGELIST, 14712 TB_GETHOTIMAGELIST, 14713 TB_SETDISABLEDIMAGELIST, 14714 TB_GETDISABLEDIMAGELIST, 14715 TB_SETSTYLE, 14716 TB_GETSTYLE, 14717 //TB_GETBUTTONSIZE, 14718 //TB_SETBUTTONWIDTH, 14719 TB_SETMAXTEXTROWS, 14720 TB_GETTEXTROWS // = WM_USER + 61 14721 } 14722 14723 enum { 14724 CCM_FIRST = 0x2000, 14725 CCM_LAST = CCM_FIRST + 0x200, 14726 CCM_SETBKCOLOR = 8193, 14727 CCM_SETCOLORSCHEME = 8194, 14728 CCM_GETCOLORSCHEME = 8195, 14729 CCM_GETDROPTARGET = 8196, 14730 CCM_SETUNICODEFORMAT = 8197, 14731 CCM_GETUNICODEFORMAT = 8198, 14732 CCM_SETVERSION = 0x2007, 14733 CCM_GETVERSION = 0x2008, 14734 CCM_SETNOTIFYWINDOW = 0x2009 14735 } 14736 14737 14738 enum { 14739 PBM_SETRANGE = WM_USER + 1, 14740 PBM_SETPOS, 14741 PBM_DELTAPOS, 14742 PBM_SETSTEP, 14743 PBM_STEPIT, // = WM_USER + 5 14744 PBM_SETRANGE32 = 1030, 14745 PBM_GETRANGE, 14746 PBM_GETPOS, 14747 PBM_SETBARCOLOR, // = 1033 14748 PBM_SETBKCOLOR = CCM_SETBKCOLOR 14749 } 14750 14751 enum { 14752 PBS_SMOOTH = 1, 14753 PBS_VERTICAL = 4 14754 } 14755 14756 enum { 14757 ICC_LISTVIEW_CLASSES = 1, 14758 ICC_TREEVIEW_CLASSES = 2, 14759 ICC_BAR_CLASSES = 4, 14760 ICC_TAB_CLASSES = 8, 14761 ICC_UPDOWN_CLASS = 16, 14762 ICC_PROGRESS_CLASS = 32, 14763 ICC_HOTKEY_CLASS = 64, 14764 ICC_ANIMATE_CLASS = 128, 14765 ICC_WIN95_CLASSES = 255, 14766 ICC_DATE_CLASSES = 256, 14767 ICC_USEREX_CLASSES = 512, 14768 ICC_COOL_CLASSES = 1024, 14769 ICC_STANDARD_CLASSES = 0x00004000, 14770 } 14771 14772 enum WM_USER = 1024; 14773 } 14774 14775 version(win32_widgets) 14776 pragma(lib, "comdlg32"); 14777 14778 14779 /// 14780 enum GenericIcons : ushort { 14781 None, /// 14782 // these happen to match the win32 std icons numerically if you just subtract one from the value 14783 Cut, /// 14784 Copy, /// 14785 Paste, /// 14786 Undo, /// 14787 Redo, /// 14788 Delete, /// 14789 New, /// 14790 Open, /// 14791 Save, /// 14792 PrintPreview, /// 14793 Properties, /// 14794 Help, /// 14795 Find, /// 14796 Replace, /// 14797 Print, /// 14798 } 14799 14800 enum FileDialogType { 14801 Automatic, 14802 Open, 14803 Save 14804 } 14805 string previousFileReferenced; 14806 14807 /++ 14808 Used in automatic menu functions to indicate that the user should be able to browse for a file. 14809 14810 Params: 14811 storage = an alias to a `static string` variable that stores the last file referenced. It will 14812 use this to pre-fill the dialog with a suggestion. 14813 14814 Please note that it MUST be `static` or you will get compile errors. 14815 14816 filters = the filters param to [getFileName] 14817 14818 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 14819 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 14820 a save dialog box. Otherwise, it will show an open dialog box. 14821 +/ 14822 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 14823 string name; 14824 alias name this; 14825 } 14826 14827 /++ 14828 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. 14829 14830 History: 14831 onCancel was added November 6, 2021. 14832 14833 The dialog itself on Linux was modified on December 2, 2021 to include 14834 a directory picker in addition to the command line completion view. 14835 14836 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 14837 Future_directions: 14838 I want to add some kind of custom preview and maybe thumbnail thing in the future, 14839 at least on Linux, maybe on Windows too. 14840 +/ 14841 void getOpenFileName( 14842 void delegate(string) onOK, 14843 string prefilledName = null, 14844 string[] filters = null, 14845 void delegate() onCancel = null, 14846 string initialDirectory = null, 14847 ) 14848 { 14849 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 14850 } 14851 14852 /// ditto 14853 void getSaveFileName( 14854 void delegate(string) onOK, 14855 string prefilledName = null, 14856 string[] filters = null, 14857 void delegate() onCancel = null, 14858 string initialDirectory = null, 14859 ) 14860 { 14861 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 14862 } 14863 14864 void getFileName( 14865 bool openOrSave, 14866 void delegate(string) onOK, 14867 string prefilledName = null, 14868 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 14869 void delegate() onCancel = null, 14870 string initialDirectory = null, 14871 ) 14872 { 14873 14874 version(win32_widgets) { 14875 import core.sys.windows.commdlg; 14876 /* 14877 Ofn.lStructSize = sizeof(OPENFILENAME); 14878 Ofn.hwndOwner = hWnd; 14879 Ofn.lpstrFilter = szFilter; 14880 Ofn.lpstrFile= szFile; 14881 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 14882 Ofn.lpstrFileTitle = szFileTitle; 14883 Ofn.nMaxFileTitle = sizeof(szFileTitle); 14884 Ofn.lpstrInitialDir = (LPSTR)NULL; 14885 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 14886 Ofn.lpstrTitle = szTitle; 14887 */ 14888 14889 14890 wchar[1024] file = 0; 14891 wchar[1024] filterBuffer = 0; 14892 makeWindowsString(prefilledName, file[]); 14893 OPENFILENAME ofn; 14894 ofn.lStructSize = ofn.sizeof; 14895 if(filters.length) { 14896 string filter; 14897 foreach(i, f; filters) { 14898 filter ~= f; 14899 filter ~= "\0"; 14900 } 14901 filter ~= "\0"; 14902 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 14903 } 14904 ofn.lpstrFile = file.ptr; 14905 ofn.nMaxFile = file.length; 14906 14907 wchar[1024] initialDir = 0; 14908 if(initialDirectory !is null) { 14909 makeWindowsString(initialDirectory, initialDir[]); 14910 ofn.lpstrInitialDir = file.ptr; 14911 } 14912 14913 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 14914 { 14915 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 14916 if(okString.length && okString[$-1] == '\0') 14917 okString = okString[0..$-1]; 14918 onOK(okString); 14919 } else { 14920 if(onCancel) 14921 onCancel(); 14922 } 14923 } else version(custom_widgets) { 14924 if(filters.length == 0) 14925 filters = ["All Files\0*.*"]; 14926 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 14927 picker.onOK = onOK; 14928 picker.onCancel = onCancel; 14929 picker.show(); 14930 } 14931 } 14932 14933 version(custom_widgets) 14934 private 14935 class FilePicker : Dialog { 14936 void delegate(string) onOK; 14937 void delegate() onCancel; 14938 LineEdit lineEdit; 14939 14940 // returns common prefix 14941 string loadFiles(string cwd, string[] filters...) { 14942 string[] files; 14943 string[] dirs; 14944 14945 string commonPrefix; 14946 14947 getFiles(cwd, (string name, bool isDirectory) { 14948 if(name == ".") 14949 return; // skip this as unnecessary 14950 if(isDirectory) 14951 dirs ~= name; 14952 else { 14953 foreach(filter; filters) 14954 if( 14955 filter.length <= 1 || 14956 filter == "*.*" || 14957 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 14958 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 14959 ) 14960 { 14961 files ~= name; 14962 14963 if(filter.length > 0 && filter[$-1] == '*') { 14964 if(commonPrefix is null) { 14965 commonPrefix = name; 14966 } else { 14967 foreach(idx, char i; name) { 14968 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 14969 commonPrefix = commonPrefix[0 .. idx]; 14970 break; 14971 } 14972 } 14973 } 14974 } 14975 14976 break; 14977 } 14978 } 14979 }); 14980 14981 extern(C) static int comparator(scope const void* a, scope const void* b) { 14982 auto sa = *cast(string*) a; 14983 auto sb = *cast(string*) b; 14984 14985 for(int i = 0; i < sa.length; i++) { 14986 if(i == sb.length) 14987 return 1; 14988 return sa[i] - sb[i]; 14989 } 14990 14991 return 0; 14992 } 14993 14994 nonPhobosSort(files, &comparator); 14995 nonPhobosSort(dirs, &comparator); 14996 14997 listWidget.clear(); 14998 dirWidget.clear(); 14999 foreach(name; dirs) 15000 dirWidget.addOption(name); 15001 foreach(name; files) 15002 listWidget.addOption(name); 15003 15004 return commonPrefix; 15005 } 15006 15007 ListWidget listWidget; 15008 ListWidget dirWidget; 15009 15010 string currentDirectory; 15011 string[] processedFilters; 15012 15013 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 15014 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 15015 super(300, 200, "Choose File..."); // owner); 15016 15017 foreach(filter; filters) { 15018 while(filter.length && filter[0] != 0) { 15019 filter = filter[1 .. $]; 15020 } 15021 if(filter.length) 15022 filter = filter[1 .. $]; // trim off the 0 15023 15024 while(filter.length) { 15025 int idx = 0; 15026 while(idx < filter.length && filter[idx] != ';') { 15027 idx++; 15028 } 15029 15030 processedFilters ~= filter[0 .. idx]; 15031 if(idx < filter.length) 15032 idx++; // skip the ; 15033 filter = filter[idx .. $]; 15034 } 15035 } 15036 15037 currentDirectory = initialDirectory is null ? "." : initialDirectory; 15038 15039 { 15040 auto hl = new HorizontalLayout(this); 15041 dirWidget = new ListWidget(hl); 15042 listWidget = new ListWidget(hl); 15043 15044 // double click events normally trigger something else but 15045 // here user might be clicking kinda fast and we'd rather just 15046 // keep it 15047 dirWidget.addEventListener((scope DoubleClickEvent dev) { 15048 auto ce = new ChangeEvent!void(dirWidget, () {}); 15049 ce.dispatch(); 15050 }); 15051 15052 dirWidget.addEventListener((scope ChangeEvent!void sce) { 15053 string v; 15054 foreach(o; dirWidget.options) 15055 if(o.selected) { 15056 v = o.label; 15057 break; 15058 } 15059 if(v.length) { 15060 currentDirectory ~= "/" ~ v; 15061 loadFiles(currentDirectory, processedFilters); 15062 } 15063 }); 15064 15065 // double click here, on the other hand, selects the file 15066 // and moves on 15067 listWidget.addEventListener((scope DoubleClickEvent dev) { 15068 OK(); 15069 }); 15070 } 15071 15072 lineEdit = new LineEdit(this); 15073 lineEdit.focus(); 15074 lineEdit.addEventListener(delegate(CharEvent event) { 15075 if(event.character == '\t' || event.character == '\n') 15076 event.preventDefault(); 15077 }); 15078 15079 listWidget.addEventListener(EventType.change, () { 15080 foreach(o; listWidget.options) 15081 if(o.selected) 15082 lineEdit.content = o.label; 15083 }); 15084 15085 loadFiles(currentDirectory, processedFilters); 15086 15087 lineEdit.addEventListener((KeyDownEvent event) { 15088 if(event.key == Key.Tab) { 15089 15090 auto current = lineEdit.content; 15091 if(current.length >= 2 && current[0 ..2] == "./") 15092 current = current[2 .. $]; 15093 15094 auto commonPrefix = loadFiles(".", current ~ "*"); 15095 15096 if(commonPrefix.length) 15097 lineEdit.content = commonPrefix; 15098 15099 // FIXME: if that is a directory, add the slash? or even go inside? 15100 15101 event.preventDefault(); 15102 } 15103 }); 15104 15105 lineEdit.content = prefilledName; 15106 15107 auto hl = new HorizontalLayout(60, this); 15108 auto cancelButton = new Button("Cancel", hl); 15109 auto okButton = new Button("OK", hl); 15110 15111 cancelButton.addEventListener(EventType.triggered, &Cancel); 15112 okButton.addEventListener(EventType.triggered, &OK); 15113 15114 this.addEventListener((KeyDownEvent event) { 15115 if(event.key == Key.Enter || event.key == Key.PadEnter) { 15116 event.preventDefault(); 15117 OK(); 15118 } 15119 if(event.key == Key.Escape) 15120 Cancel(); 15121 }); 15122 15123 } 15124 15125 override void OK() { 15126 if(lineEdit.content.length) { 15127 string accepted; 15128 auto c = lineEdit.content; 15129 if(c.length && c[0] == '/') 15130 accepted = c; 15131 else 15132 accepted = currentDirectory ~ "/" ~ lineEdit.content; 15133 15134 if(isDir(accepted)) { 15135 // FIXME: would be kinda nice to support ~ and collapse these paths too 15136 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 15137 currentDirectory = accepted; 15138 loadFiles(currentDirectory, processedFilters); 15139 lineEdit.content = ""; 15140 return; 15141 } 15142 15143 if(onOK) 15144 onOK(accepted); 15145 } 15146 close(); 15147 } 15148 15149 override void Cancel() { 15150 if(onCancel) 15151 onCancel(); 15152 close(); 15153 } 15154 } 15155 15156 private bool isDir(string name) { 15157 version(Windows) { 15158 auto ws = WCharzBuffer(name); 15159 auto ret = GetFileAttributesW(ws.ptr); 15160 if(ret == INVALID_FILE_ATTRIBUTES) 15161 return false; 15162 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 15163 } else version(Posix) { 15164 import core.sys.posix.sys.stat; 15165 stat_t buf; 15166 auto ret = stat((name ~ '\0').ptr, &buf); 15167 if(ret == -1) 15168 return false; // I could probably check more specific errors tbh 15169 return (buf.st_mode & S_IFMT) == S_IFDIR; 15170 } else return false; 15171 } 15172 15173 /* 15174 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 15175 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 15176 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 15177 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 15178 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 15179 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 15180 http://www.sbin.org/doc/Xlib/chapt_03.html 15181 15182 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 15183 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 15184 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 15185 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 15186 */ 15187 15188 15189 // These are all for setMenuAndToolbarFromAnnotatedCode 15190 /// This item in the menu will be preceded by a separator line 15191 /// Group: generating_from_code 15192 struct separator {} 15193 deprecated("It was misspelled, use separator instead") alias seperator = separator; 15194 /// Program-wide keyboard shortcut to trigger the action 15195 /// Group: generating_from_code 15196 struct accelerator { string keyString; } 15197 /// tells which menu the action will be on 15198 /// Group: generating_from_code 15199 struct menu { string name; } 15200 /// Describes which toolbar section the action appears on 15201 /// Group: generating_from_code 15202 struct toolbar { string groupName; } 15203 /// 15204 /// Group: generating_from_code 15205 struct icon { ushort id; } 15206 /// 15207 /// Group: generating_from_code 15208 struct label { string label; } 15209 /// 15210 /// Group: generating_from_code 15211 struct hotkey { dchar ch; } 15212 /// 15213 /// Group: generating_from_code 15214 struct tip { string tip; } 15215 15216 15217 /++ 15218 Observes and allows inspection of an object via automatic gui 15219 +/ 15220 /// Group: generating_from_code 15221 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 15222 return new ObjectInspectionWindowImpl!(T)(t); 15223 } 15224 15225 class ObjectInspectionWindow : Window { 15226 this(int a, int b, string c) { 15227 super(a, b, c); 15228 } 15229 15230 abstract void readUpdatesFromObject(); 15231 } 15232 15233 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 15234 T t; 15235 this(T t) { 15236 this.t = t; 15237 15238 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 15239 15240 foreach(memberName; __traits(derivedMembers, T)) {{ 15241 alias member = I!(__traits(getMember, t, memberName))[0]; 15242 alias type = typeof(member); 15243 static if(is(type == int)) { 15244 auto le = new LabeledLineEdit(memberName ~ ": ", this); 15245 //le.addEventListener("char", (Event ev) { 15246 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 15247 //ev.preventDefault(); 15248 //}); 15249 le.addEventListener(EventType.change, (Event ev) { 15250 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 15251 }); 15252 15253 updateMemberDelegates[memberName] = () { 15254 le.content = toInternal!string(__traits(getMember, t, memberName)); 15255 }; 15256 } 15257 }} 15258 } 15259 15260 void delegate()[string] updateMemberDelegates; 15261 15262 override void readUpdatesFromObject() { 15263 foreach(k, v; updateMemberDelegates) 15264 v(); 15265 } 15266 } 15267 15268 /++ 15269 Creates a dialog based on a data structure. 15270 15271 --- 15272 dialog((YourStructure value) { 15273 // the user filled in the struct and clicked OK, 15274 // you can check the members now 15275 }); 15276 --- 15277 15278 Params: 15279 initialData = the initial value to show in the dialog. It will not modify this unless 15280 it is a class then it might, no promises. 15281 15282 History: 15283 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 15284 +/ 15285 /// Group: generating_from_code 15286 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15287 dialog(T.init, onOK, onCancel, title); 15288 } 15289 /// ditto 15290 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15291 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 15292 dg.show(); 15293 } 15294 15295 private static template I(T...) { alias I = T; } 15296 15297 15298 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 15299 if(name == "id") 15300 return allLowerCase ? name : "ID"; 15301 15302 char[160] buffer; 15303 int bufferIndex = 0; 15304 bool shouldCap = true; 15305 bool shouldSpace; 15306 bool lastWasCap; 15307 foreach(idx, char ch; name) { 15308 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15309 15310 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 15311 if(lastWasCap) { 15312 // two caps in a row, don't change. Prolly acronym. 15313 } else { 15314 if(idx) 15315 shouldSpace = true; // new word, add space 15316 } 15317 15318 lastWasCap = true; 15319 } else { 15320 lastWasCap = false; 15321 } 15322 15323 if(shouldSpace) { 15324 buffer[bufferIndex++] = space; 15325 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15326 shouldSpace = false; 15327 } 15328 if(shouldCap) { 15329 if(ch >= 'a' && ch <= 'z') 15330 ch -= 32; 15331 shouldCap = false; 15332 } 15333 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15334 ch += 32; 15335 buffer[bufferIndex++] = ch; 15336 } 15337 return buffer[0 .. bufferIndex].idup; 15338 } 15339 15340 /++ 15341 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. 15342 +/ 15343 class AutomaticDialog(T) : Dialog { 15344 T t; 15345 15346 void delegate(T) onOK; 15347 void delegate() onCancel; 15348 15349 override int paddingTop() { return defaultLineHeight; } 15350 override int paddingBottom() { return defaultLineHeight; } 15351 override int paddingRight() { return defaultLineHeight; } 15352 override int paddingLeft() { return defaultLineHeight; } 15353 15354 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15355 assert(onOK !is null); 15356 15357 t = initialData; 15358 15359 static if(is(T == class)) { 15360 if(t is null) 15361 t = new T(); 15362 } 15363 this.onOK = onOK; 15364 this.onCancel = onCancel; 15365 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 15366 15367 static if(is(T == class)) 15368 this.addDataControllerWidget(t); 15369 else 15370 this.addDataControllerWidget(&t); 15371 15372 auto hl = new HorizontalLayout(this); 15373 auto stretch = new HorizontalSpacer(hl); // to right align 15374 auto ok = new CommandButton("OK", hl); 15375 auto cancel = new CommandButton("Cancel", hl); 15376 ok.addEventListener(EventType.triggered, &OK); 15377 cancel.addEventListener(EventType.triggered, &Cancel); 15378 15379 this.addEventListener((KeyDownEvent ev) { 15380 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15381 ok.focus(); 15382 OK(); 15383 ev.preventDefault(); 15384 } 15385 if(ev.key == Key.Escape) { 15386 Cancel(); 15387 ev.preventDefault(); 15388 } 15389 }); 15390 15391 this.addEventListener((scope ClosedEvent ce) { 15392 if(onCancel) 15393 onCancel(); 15394 }); 15395 15396 //this.children[0].focus(); 15397 } 15398 15399 override void OK() { 15400 onOK(t); 15401 close(); 15402 } 15403 15404 override void Cancel() { 15405 if(onCancel) 15406 onCancel(); 15407 close(); 15408 } 15409 } 15410 15411 private template baseClassCount(Class) { 15412 private int helper() { 15413 int count = 0; 15414 static if(is(Class bases == super)) { 15415 foreach(base; bases) 15416 static if(is(base == class)) 15417 count += 1 + baseClassCount!base; 15418 } 15419 return count; 15420 } 15421 15422 enum int baseClassCount = helper(); 15423 } 15424 15425 private long stringToLong(string s) { 15426 long ret; 15427 if(s.length == 0) 15428 return ret; 15429 bool negative = s[0] == '-'; 15430 if(negative) 15431 s = s[1 .. $]; 15432 foreach(ch; s) { 15433 if(ch >= '0' && ch <= '9') { 15434 ret *= 10; 15435 ret += ch - '0'; 15436 } 15437 } 15438 if(negative) 15439 ret = -ret; 15440 return ret; 15441 } 15442 15443 15444 interface ReflectableProperties { 15445 /++ 15446 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15447 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15448 json in the current implementation. 15449 15450 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15451 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15452 as of the June 2, 2021 release. 15453 15454 History: 15455 Added June 2, 2021. 15456 15457 See_Also: [getPropertyAsString], [setPropertyFromString] 15458 +/ 15459 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15460 /++ 15461 Requests a property to be delivered to you as a string, through your `sink` delegate. 15462 15463 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15464 be interpreted as json, otherwise, it is just a plain string. 15465 15466 The sink should always be called exactly once for each call (it is basically a return value, but it might 15467 use a local buffer it maintains instead of allocating a return value). 15468 15469 History: 15470 Added June 2, 2021. 15471 15472 See_Also: [getPropertiesList], [setPropertyFromString] 15473 +/ 15474 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15475 /++ 15476 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. 15477 15478 History: 15479 Added June 2, 2021. 15480 15481 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15482 +/ 15483 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15484 15485 /// [setPropertyFromString] possible return values 15486 enum SetPropertyResult { 15487 success = 0, /// the property has been successfully set to the request value 15488 notPermitted = -1, /// the property exists but it cannot be changed at this time 15489 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15490 noSuchProperty = -3, /// there is no property by that name 15491 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15492 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) 15493 } 15494 15495 /++ 15496 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15497 15498 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15499 15500 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15501 rarely need to use these building blocks directly. 15502 +/ 15503 mixin template RegisterSetters() { 15504 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15505 switch(name) { 15506 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15507 case memberName: 15508 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15509 if(value != "true" && value != "false") 15510 return SetPropertyResult.wrongFormat; 15511 __traits(getMember, this, memberName) = value == "true" ? true : false; 15512 return SetPropertyResult.success; 15513 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15514 import core.stdc.stdlib; 15515 char[128] zero = 0; 15516 if(buffer.length + 1 >= zero.length) 15517 return SetPropertyResult.wrongFormat; 15518 zero[0 .. buffer.length] = buffer[]; 15519 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15520 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15521 import core.stdc.stdlib; 15522 char[128] zero = 0; 15523 if(buffer.length + 1 >= zero.length) 15524 return SetPropertyResult.wrongFormat; 15525 zero[0 .. buffer.length] = buffer[]; 15526 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15527 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15528 __traits(getMember, this, memberName) = value.idup; 15529 } else { 15530 return SetPropertyResult.notImplemented; 15531 } 15532 15533 } 15534 default: 15535 return super.setPropertyFromString(name, value, valueIsJson); 15536 } 15537 } 15538 } 15539 15540 /++ 15541 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15542 15543 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15544 15545 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15546 rarely need to use these building blocks directly. 15547 +/ 15548 mixin template RegisterGetters() { 15549 override void getPropertiesList(scope void delegate(string name) sink) const { 15550 super.getPropertiesList(sink); 15551 15552 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15553 sink(memberName); 15554 } 15555 } 15556 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15557 switch(name) { 15558 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15559 case memberName: 15560 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15561 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15562 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15563 import core.stdc.stdio; 15564 char[32] buffer; 15565 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15566 sink(name, buffer[0 .. len], true); 15567 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15568 import core.stdc.stdio; 15569 char[32] buffer; 15570 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15571 sink(name, buffer[0 .. len], true); 15572 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15573 sink(name, __traits(getMember, this, memberName), false); 15574 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15575 } else { 15576 sink(name, null, true); 15577 } 15578 15579 return; 15580 } 15581 default: 15582 return super.getPropertyAsString(name, sink); 15583 } 15584 } 15585 } 15586 } 15587 15588 private struct Stack(T) { 15589 this(int maxSize) { 15590 internalLength = 0; 15591 arr = initialBuffer[]; 15592 } 15593 15594 ///. 15595 void push(T t) { 15596 if(internalLength >= arr.length) { 15597 auto oldarr = arr; 15598 if(arr.length < 4096) 15599 arr = new T[arr.length * 2]; 15600 else 15601 arr = new T[arr.length + 4096]; 15602 arr[0 .. oldarr.length] = oldarr[]; 15603 } 15604 15605 arr[internalLength] = t; 15606 internalLength++; 15607 } 15608 15609 ///. 15610 T pop() { 15611 assert(internalLength); 15612 internalLength--; 15613 return arr[internalLength]; 15614 } 15615 15616 ///. 15617 T peek() { 15618 assert(internalLength); 15619 return arr[internalLength - 1]; 15620 } 15621 15622 ///. 15623 @property bool empty() { 15624 return internalLength ? false : true; 15625 } 15626 15627 ///. 15628 private T[] arr; 15629 private size_t internalLength; 15630 private T[64] initialBuffer; 15631 // 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), 15632 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 15633 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 15634 } 15635 15636 /// 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. 15637 private struct WidgetStream { 15638 15639 ///. 15640 @property Widget front() { 15641 return current.widget; 15642 } 15643 15644 /// Use Widget.tree instead. 15645 this(Widget start) { 15646 current.widget = start; 15647 current.childPosition = -1; 15648 isEmpty = false; 15649 stack = typeof(stack)(0); 15650 } 15651 15652 /* 15653 Handle it 15654 handle its children 15655 15656 */ 15657 15658 ///. 15659 void popFront() { 15660 more: 15661 if(isEmpty) return; 15662 15663 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 15664 15665 current.childPosition++; 15666 if(current.childPosition >= current.widget.children.length) { 15667 if(stack.empty()) 15668 isEmpty = true; 15669 else { 15670 current = stack.pop(); 15671 goto more; 15672 } 15673 } else { 15674 stack.push(current); 15675 current.widget = current.widget.children[current.childPosition]; 15676 current.childPosition = -1; 15677 } 15678 } 15679 15680 ///. 15681 @property bool empty() { 15682 return isEmpty; 15683 } 15684 15685 private: 15686 15687 struct Current { 15688 Widget widget; 15689 int childPosition; 15690 } 15691 15692 Current current; 15693 15694 Stack!(Current) stack; 15695 15696 bool isEmpty; 15697 } 15698 15699 15700 /+ 15701 15702 I could fix up the hierarchy kinda like this 15703 15704 class Widget { 15705 Widget[] children() { return null; } 15706 } 15707 interface WidgetContainer { 15708 Widget asWidget(); 15709 void addChild(Widget w); 15710 15711 // alias asWidget this; // but meh 15712 } 15713 15714 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 15715 15716 class Layout : Widget, WidgetContainer {} 15717 15718 class Window : WidgetContainer {} 15719 15720 15721 All constructors that previously took Widgets should now take WidgetContainers instead 15722 15723 15724 15725 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". 15726 +/ 15727 15728 /+ 15729 LAYOUTS 2.0 15730 15731 can just be assigned as a function. assigning a new one will cause it to be immediately called. 15732 15733 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 15734 15735 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 15736 15737 and even Paint can just use computedStyle... 15738 15739 background color 15740 font 15741 border color and style 15742 15743 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 15744 please note that many widgets and in some modes will completely ignore properties as they will. 15745 they are just hints you set, not promises. 15746 15747 15748 15749 15750 15751 So generally the existing virtual functions are just the default for the class. But individual objects 15752 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 15753 +/ 15754 15755 /++ 15756 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. 15757 15758 History: 15759 Added May 24, 2021. 15760 +/ 15761 struct WidgetBackground { 15762 /++ 15763 A background with the given solid color. 15764 +/ 15765 this(Color color) { 15766 this.color = color; 15767 } 15768 15769 this(WidgetBackground bg) { 15770 this = bg; 15771 } 15772 15773 /++ 15774 Creates a widget from the string. 15775 15776 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 15777 +/ 15778 static WidgetBackground fromString(string s) { 15779 return WidgetBackground(Color.fromString(s)); 15780 } 15781 15782 /++ 15783 The background is not necessarily a solid color, but you can always specify a color as a fallback. 15784 15785 History: 15786 Made `public` on December 18, 2022 (dub v10.10). 15787 +/ 15788 Color color; 15789 } 15790 15791 /++ 15792 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!) 15793 15794 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. 15795 15796 You should not inherit from this directly, but instead use [VisualTheme]. 15797 15798 History: 15799 Added May 8, 2021 15800 +/ 15801 abstract class BaseVisualTheme { 15802 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 15803 abstract void doPaint(Widget widget, WidgetPainter painter); 15804 15805 /+ 15806 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 15807 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 15808 +/ 15809 15810 /++ 15811 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 15812 where the interpretation of the string varies for each property and may include things like measurement units. 15813 +/ 15814 abstract string getPropertyString(Widget widget, string propertyName); 15815 15816 /++ 15817 Default background color of the window. Widgets also use this to simulate transparency. 15818 15819 Probably some shade of grey. 15820 +/ 15821 abstract Color windowBackgroundColor(); 15822 abstract Color widgetBackgroundColor(); 15823 abstract Color foregroundColor(); 15824 abstract Color lightAccentColor(); 15825 abstract Color darkAccentColor(); 15826 15827 /++ 15828 Colors used to indicate active selections in lists and text boxes, etc. 15829 +/ 15830 abstract Color selectionForegroundColor(); 15831 /// ditto 15832 abstract Color selectionBackgroundColor(); 15833 15834 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 15835 15836 /++ 15837 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 15838 +/ 15839 abstract OperatingSystemFont defaultFont(int dpi); 15840 15841 private OperatingSystemFont[int] defaultFontCache_; 15842 private OperatingSystemFont defaultFontCached(int dpi) { 15843 if(dpi !in defaultFontCache_) { 15844 // FIXME: set this to false if X disconnect or if visual theme changes 15845 defaultFontCache_[dpi] = defaultFont(dpi); 15846 } 15847 return defaultFontCache_[dpi]; 15848 } 15849 } 15850 15851 /+ 15852 A widget should have: 15853 classList 15854 dataset 15855 attributes 15856 computedStyles 15857 state (persistent) 15858 dynamic state (focused, hover, etc) 15859 +/ 15860 15861 // visualTheme.computedStyle(this).paddingLeft 15862 15863 15864 /++ 15865 This is your entry point to create your own visual theme for custom widgets. 15866 15867 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 15868 15869 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 15870 +/ 15871 abstract class VisualTheme(CRTP) : BaseVisualTheme { 15872 override string getPropertyString(Widget widget, string propertyName) { 15873 return null; 15874 } 15875 15876 /+ 15877 mixin StyleOverride!Widget 15878 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 15879 w.useStyleProperties(dg); 15880 } 15881 +/ 15882 15883 final override void doPaint(Widget widget, WidgetPainter painter) { 15884 auto derived = cast(CRTP) cast(void*) this; 15885 15886 scope void delegate(Widget, WidgetPainter) bestMatch; 15887 int bestMatchScore; 15888 15889 static if(__traits(hasMember, CRTP, "paint")) 15890 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 15891 static if(is(typeof(overload) Params == __parameters)) { 15892 static assert(Params.length == 2); 15893 static assert(is(Params[0] : Widget)); 15894 static assert(is(Params[1] == WidgetPainter)); 15895 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); 15896 15897 alias type = Params[0]; 15898 if(cast(type) widget) { 15899 auto score = baseClassCount!type; 15900 15901 if(score > bestMatchScore) { 15902 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 15903 bestMatchScore = score; 15904 } 15905 } 15906 } else static assert(0, "paint should be a method."); 15907 } 15908 15909 if(bestMatch) 15910 bestMatch(widget, painter); 15911 else 15912 widget.paint(painter); 15913 } 15914 15915 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 15916 15917 // 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 15918 // mixin Beautiful95Theme; 15919 mixin DefaultLightTheme; 15920 15921 private static struct Cached { 15922 // i prolly want to do this 15923 } 15924 } 15925 15926 /// ditto 15927 mixin template Beautiful95Theme() { 15928 override Color windowBackgroundColor() { return Color(212, 212, 212); } 15929 override Color widgetBackgroundColor() { return Color.white; } 15930 override Color foregroundColor() { return Color.black; } 15931 override Color darkAccentColor() { return Color(172, 172, 172); } 15932 override Color lightAccentColor() { return Color(223, 223, 223); } 15933 override Color selectionForegroundColor() { return Color.white; } 15934 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15935 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 15936 } 15937 15938 /// ditto 15939 mixin template DefaultLightTheme() { 15940 override Color windowBackgroundColor() { return Color(232, 232, 232); } 15941 override Color widgetBackgroundColor() { return Color.white; } 15942 override Color foregroundColor() { return Color.black; } 15943 override Color darkAccentColor() { return Color(172, 172, 172); } 15944 override Color lightAccentColor() { return Color(223, 223, 223); } 15945 override Color selectionForegroundColor() { return Color.white; } 15946 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15947 override OperatingSystemFont defaultFont(int dpi) { 15948 version(Windows) 15949 return new OperatingSystemFont("Segoe UI"); 15950 else { 15951 // FIXME: undo xft's scaling so we don't end up double scaled 15952 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15953 } 15954 } 15955 } 15956 15957 /// ditto 15958 mixin template DefaultDarkTheme() { 15959 override Color windowBackgroundColor() { return Color(64, 64, 64); } 15960 override Color widgetBackgroundColor() { return Color.black; } 15961 override Color foregroundColor() { return Color.white; } 15962 override Color darkAccentColor() { return Color(20, 20, 20); } 15963 override Color lightAccentColor() { return Color(80, 80, 80); } 15964 override Color selectionForegroundColor() { return Color.white; } 15965 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 15966 override OperatingSystemFont defaultFont(int dpi) { 15967 version(Windows) 15968 return new OperatingSystemFont("Segoe UI", 12); 15969 else 15970 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15971 } 15972 } 15973 15974 /// ditto 15975 alias DefaultTheme = DefaultLightTheme; 15976 15977 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 15978 /+ 15979 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 15980 Color windowBackgroundColor() { return Color(242, 242, 242); } 15981 Color darkAccentColor() { return windowBackgroundColor; } 15982 Color lightAccentColor() { return windowBackgroundColor; } 15983 +/ 15984 } 15985 15986 /++ 15987 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 15988 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 15989 15990 History: 15991 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15992 +/ 15993 class StateChanged(alias field) : Event { 15994 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 15995 override bool cancelable() const { return false; } 15996 this(Widget target, typeof(field) newValue) { 15997 this.newValue = newValue; 15998 super(EventString, target); 15999 } 16000 16001 typeof(field) newValue; 16002 } 16003 16004 /++ 16005 Convenience function to add a `triggered` event listener. 16006 16007 Its implementation is simply `w.addEventListener("triggered", dg);` 16008 16009 History: 16010 Added November 27, 2021 (dub v10.4) 16011 +/ 16012 void addWhenTriggered(Widget w, void delegate() dg) { 16013 w.addEventListener("triggered", dg); 16014 } 16015 16016 /++ 16017 Observable varables can be added to widgets and when they are changed, it fires 16018 off a [StateChanged] event so you can react to it. 16019 16020 It is implemented as a getter and setter property, along with another helper you 16021 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 16022 event through the usual means. Just give the name of the variable. See [StateChanged] for an 16023 example. 16024 16025 History: 16026 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 16027 +/ 16028 mixin template Observable(T, string name) { 16029 private T backing; 16030 16031 mixin(q{ 16032 void } ~ name ~ q{_changed (void delegate(T) dg) { 16033 this.addEventListener((StateChanged!this_thing ev) { 16034 dg(ev.newValue); 16035 }); 16036 } 16037 16038 @property T } ~ name ~ q{ () { 16039 return backing; 16040 } 16041 16042 @property void } ~ name ~ q{ (T t) { 16043 backing = t; 16044 auto event = new StateChanged!this_thing(this, t); 16045 event.dispatch(); 16046 } 16047 }); 16048 16049 mixin("private alias this_thing = " ~ name ~ ";"); 16050 } 16051 16052 16053 private bool startsWith(string test, string thing) { 16054 if(test.length < thing.length) 16055 return false; 16056 return test[0 .. thing.length] == thing; 16057 } 16058 16059 private bool endsWith(string test, string thing) { 16060 if(test.length < thing.length) 16061 return false; 16062 return test[$ - thing.length .. $] == thing; 16063 } 16064 16065 // still do layout delegation 16066 // and... split off Window from Widget. 16067 16068 version(minigui_screenshots) 16069 struct Screenshot { 16070 string name; 16071 } 16072 16073 version(minigui_screenshots) 16074 static if(__VERSION__ > 2092) 16075 mixin(q{ 16076 shared static this() { 16077 import core.runtime; 16078 16079 static UnitTestResult screenshotMagic() { 16080 string name; 16081 16082 import arsd.png; 16083 16084 auto results = new Window(); 16085 auto button = new Button("do it", results); 16086 16087 Window.newWindowCreated = delegate(Window w) { 16088 Timer timer; 16089 timer = new Timer(250, { 16090 auto img = w.win.takeScreenshot(); 16091 timer.destroy(); 16092 16093 version(Windows) 16094 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 16095 else 16096 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 16097 16098 w.close(); 16099 }); 16100 }; 16101 16102 button.addWhenTriggered( { 16103 16104 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 16105 name = null; 16106 static foreach(attr; __traits(getAttributes, test)) { 16107 static if(is(typeof(attr) == Screenshot)) 16108 name = attr.name; 16109 } 16110 if(name.length) { 16111 test(); 16112 } 16113 } 16114 16115 }); 16116 16117 results.loop(); 16118 16119 return UnitTestResult(0, 0, false, false); 16120 } 16121 16122 16123 Runtime.extendedModuleUnitTester = &screenshotMagic; 16124 } 16125 }); 16126 version(minigui_screenshots) { 16127 version(unittest) 16128 void main() {} 16129 else static assert(0, "dont forget the -unittest flag to dmd"); 16130 } 16131 16132 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 16133 // FIXME: make multiple accelerators disambiguate based ona rgs 16134 // FIXME: MainWindow ctor should have same arg order as Window 16135 // FIXME: mainwindow ctor w/ client area size instead of total size. 16136 // 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. 16137 // FIXME: tri-state checkbox 16138 // FIXME: subordinate controls grouping...