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, false); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 1603 } 1604 } 1605 sendResizeEvent(); 1606 } 1607 1608 /// Creates the widget and adds it to the parent. 1609 this(Widget parent) { 1610 if(parent !is null) 1611 parent.addChild(this); 1612 setupDefaultEventHandlers(); 1613 } 1614 1615 /// 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. 1616 @scriptable 1617 bool isFocused() { 1618 return parentWindow && parentWindow.focusedWidget is this; 1619 } 1620 1621 private bool showing_ = true; 1622 /// 1623 bool showing() { return showing_; } 1624 /// 1625 bool hidden() { return !showing_; } 1626 /++ 1627 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. 1628 +/ 1629 void showing(bool s, bool recalculate = true) { 1630 auto so = showing_; 1631 showing_ = s; 1632 if(s != so) { 1633 version(win32_widgets) 1634 if(hwnd) 1635 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1636 1637 if(parent && recalculate) { 1638 parent.queueRecomputeChildLayout(); 1639 parent.redraw(); 1640 } 1641 1642 foreach(child; children) 1643 child.showing(s, false); 1644 1645 } 1646 queueRecomputeChildLayout(); 1647 redraw(); 1648 } 1649 /// Convenience method for `showing = true` 1650 @scriptable 1651 void show() { 1652 showing = true; 1653 } 1654 /// Convenience method for `showing = false` 1655 @scriptable 1656 void hide() { 1657 showing = false; 1658 } 1659 1660 /// 1661 @scriptable 1662 void focus() { 1663 assert(parentWindow !is null); 1664 if(isFocused()) 1665 return; 1666 1667 if(parentWindow.focusedWidget) { 1668 // FIXME: more details here? like from and to 1669 auto from = parentWindow.focusedWidget; 1670 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1671 parentWindow.focusedWidget = null; 1672 from.emit!BlurEvent(); 1673 this.emit!FocusOutEvent(); 1674 } 1675 1676 1677 version(win32_widgets) { 1678 if(this.hwnd !is null) 1679 SetFocus(this.hwnd); 1680 } 1681 //else static if(UsingSimpledisplayX11) 1682 //this.parentWindow.win.focus(); 1683 1684 parentWindow.focusedWidget = this; 1685 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1686 this.emit!FocusEvent(); 1687 this.emit!FocusInEvent(); 1688 } 1689 1690 /+ 1691 /++ 1692 Unfocuses the widget. This may reset 1693 +/ 1694 @scriptable 1695 void blur() { 1696 1697 } 1698 +/ 1699 1700 1701 /++ 1702 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1703 1704 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1705 +/ 1706 void attachedToWindow(Window w) {} 1707 /++ 1708 Callback when the widget is added to another widget. 1709 1710 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1711 +/ 1712 void addedTo(Widget w) {} 1713 1714 /++ 1715 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. 1716 1717 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1718 +/ 1719 protected void addChild(Widget w, int position = int.max) { 1720 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1721 assert(w !is this, "Child cannot be its own parent!"); 1722 w._parent = this; 1723 if(position == int.max || position == children.length) { 1724 _children ~= w; 1725 } else { 1726 assert(position < _children.length); 1727 _children.length = _children.length + 1; 1728 for(int i = cast(int) _children.length - 1; i > position; i--) 1729 _children[i] = _children[i - 1]; 1730 _children[position] = w; 1731 } 1732 1733 this.parentWindow = this._parentWindow; 1734 1735 w.addedTo(this); 1736 1737 if(this.hidden) 1738 w.showing = false; 1739 1740 if(parentWindow !is null) { 1741 w.attachedToWindow(parentWindow); 1742 parentWindow.queueRecomputeChildLayout(); 1743 parentWindow.redraw(); 1744 } 1745 } 1746 1747 /++ 1748 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. 1749 +/ 1750 Widget getChildAtPosition(int x, int y) { 1751 // it goes backward so the last one to show gets picked first 1752 // might use z-index later 1753 foreach_reverse(child; children) { 1754 if(child.hidden) 1755 continue; 1756 if(child.x <= x && child.y <= y 1757 && ((x - child.x) < child.width) 1758 && ((y - child.y) < child.height)) 1759 { 1760 return child; 1761 } 1762 } 1763 1764 return null; 1765 } 1766 1767 /++ 1768 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. 1769 1770 History: 1771 Added July 2, 2021 (v10.2) 1772 +/ 1773 protected void addScrollPosition(ref int x, ref int y) {}; 1774 1775 /++ 1776 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. 1777 1778 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. 1779 1780 [paint] is not called for system widgets as the OS library draws them instead. 1781 1782 1783 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. 1784 1785 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1786 1787 History: 1788 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. 1789 +/ 1790 void paint(WidgetPainter painter) { 1791 version(win32_widgets) 1792 if(hwnd) { 1793 return; 1794 } 1795 painter.drawThemed(&paintContent); // note this refers to the following overload 1796 } 1797 1798 /++ 1799 Responsible for drawing the content as the theme engine is responsible for other elements. 1800 1801 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1802 1803 Params: 1804 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. 1805 1806 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. 1807 1808 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. 1809 1810 Returns: 1811 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. 1812 1813 History: 1814 Added May 15, 2021 1815 +/ 1816 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1817 return bounds; 1818 } 1819 1820 deprecated("Change ScreenPainter to WidgetPainter") 1821 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1822 1823 /// I don't actually like the name of this 1824 /// this draws a background on it 1825 void erase(WidgetPainter painter) { 1826 version(win32_widgets) 1827 if(hwnd) return; // Windows will do it. I think. 1828 1829 auto c = getComputedStyle().background.color; 1830 painter.fillColor = c; 1831 painter.outlineColor = c; 1832 1833 version(win32_widgets) { 1834 HANDLE b, p; 1835 if(c.a == 0 && parent is parentWindow) { 1836 // I don't remember why I had this really... 1837 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1838 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1839 } 1840 } 1841 painter.drawRectangle(Point(0, 0), width, height); 1842 version(win32_widgets) { 1843 if(c.a == 0 && parent is parentWindow) { 1844 SelectObject(painter.impl.hdc, p); 1845 SelectObject(painter.impl.hdc, b); 1846 } 1847 } 1848 } 1849 1850 /// 1851 WidgetPainter draw() { 1852 int x = this.x, y = this.y; 1853 auto parent = this.parent; 1854 while(parent) { 1855 x += parent.x; 1856 y += parent.y; 1857 parent = parent.parent; 1858 } 1859 1860 auto painter = parentWindow.win.draw(true); 1861 painter.originX = x; 1862 painter.originY = y; 1863 painter.setClipRectangle(Point(0, 0), width, height); 1864 return WidgetPainter(painter, this); 1865 } 1866 1867 /// 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. 1868 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1869 if(hidden) 1870 return; 1871 1872 int paintX = x; 1873 int paintY = y; 1874 if(this.useNativeDrawing()) { 1875 paintX = 0; 1876 paintY = 0; 1877 lox = 0; 1878 loy = 0; 1879 containment = Rectangle(0, 0, int.max, int.max); 1880 } 1881 1882 painter.originX = lox + paintX; 1883 painter.originY = loy + paintY; 1884 1885 bool actuallyPainted = false; 1886 1887 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1888 if(clip == Rectangle.init) { 1889 // writeln(this, " clipped out"); 1890 return; 1891 } 1892 1893 bool invalidateChildren = invalidate; 1894 1895 if(redrawRequested || force) { 1896 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1897 1898 painter.drawingUpon = this; 1899 1900 erase(painter); 1901 if(painter.visualTheme) 1902 painter.visualTheme.doPaint(this, painter); 1903 else 1904 paint(painter); 1905 1906 if(invalidate) { 1907 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1908 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1909 painter.invalidateRect(region); 1910 // children are contained inside this, so no need to do extra work 1911 invalidateChildren = false; 1912 } 1913 1914 redrawRequested = false; 1915 actuallyPainted = true; 1916 } 1917 1918 foreach(child; children) { 1919 version(win32_widgets) 1920 if(child.useNativeDrawing()) continue; 1921 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1922 } 1923 1924 version(win32_widgets) 1925 foreach(child; children) { 1926 if(child.useNativeDrawing) { 1927 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1928 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 1929 } 1930 } 1931 } 1932 1933 protected bool useNativeDrawing() nothrow { 1934 version(win32_widgets) 1935 return hwnd !is null; 1936 else 1937 return false; 1938 } 1939 1940 private static class RedrawEvent {} 1941 private __gshared re = new RedrawEvent(); 1942 1943 private bool redrawRequested; 1944 /// 1945 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1946 redrawRequested = true; 1947 1948 if(this.parentWindow) { 1949 auto sw = this.parentWindow.win; 1950 assert(sw !is null); 1951 if(!sw.eventQueued!RedrawEvent) { 1952 sw.postEvent(re); 1953 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1954 } 1955 } 1956 } 1957 1958 private SimpleWindow drawableWindow; 1959 1960 /++ 1961 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. 1962 1963 Returns: 1964 `true` if you should do your default behavior. 1965 1966 History: 1967 Added May 5, 2021 1968 1969 Bugs: 1970 It does not do the static checks on gdc right now. 1971 +/ 1972 final protected bool emit(EventType, this This, Args...)(Args args) { 1973 version(GNU) {} else 1974 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1975 auto e = new EventType(this, args); 1976 e.dispatch(); 1977 return !e.defaultPrevented; 1978 } 1979 /// ditto 1980 final protected bool emit(string eventString, this This)() { 1981 auto e = new Event(eventString, this); 1982 e.dispatch(); 1983 return !e.defaultPrevented; 1984 } 1985 1986 /++ 1987 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. 1988 1989 History: 1990 Added May 5, 2021 1991 +/ 1992 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1993 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1994 return addEventListener(handler); 1995 } 1996 1997 /++ 1998 Gets the computed style properties from the visual theme. 1999 2000 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].) 2001 2002 History: 2003 Added May 8, 2021 2004 +/ 2005 final StyleInformation getComputedStyle() { 2006 return StyleInformation(this); 2007 } 2008 2009 int focusableWidgets(scope int delegate(Widget) dg) { 2010 foreach(widget; WidgetStream(this)) { 2011 if(widget.tabStop && !widget.hidden) { 2012 int result = dg(widget); 2013 if (result) 2014 return result; 2015 } 2016 } 2017 return 0; 2018 } 2019 2020 /++ 2021 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2022 for the given content box (the area between the padding) 2023 2024 History: 2025 Added January 4, 2023 (dub v11.0) 2026 +/ 2027 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2028 auto cs = getComputedStyle(); 2029 2030 auto borderWidth = getBorderWidth(cs.borderStyle); 2031 2032 auto rect = contentBox; 2033 2034 rect.left -= borderWidth; 2035 rect.right += borderWidth; 2036 rect.top -= borderWidth; 2037 rect.bottom += borderWidth; 2038 2039 auto insideBorderRect = rect; 2040 2041 rect.left -= cs.paddingLeft; 2042 rect.right += cs.paddingRight; 2043 rect.top -= cs.paddingTop; 2044 rect.bottom += cs.paddingBottom; 2045 2046 return rect; 2047 } 2048 2049 2050 // FIXME: I kinda want to hide events from implementation widgets 2051 // so it just catches them all and stops propagation... 2052 // i guess i can do it with a event listener on star. 2053 2054 mixin Emits!KeyDownEvent; /// 2055 mixin Emits!KeyUpEvent; /// 2056 mixin Emits!CharEvent; /// 2057 2058 mixin Emits!MouseDownEvent; /// 2059 mixin Emits!MouseUpEvent; /// 2060 mixin Emits!ClickEvent; /// 2061 mixin Emits!DoubleClickEvent; /// 2062 mixin Emits!MouseMoveEvent; /// 2063 mixin Emits!MouseOverEvent; /// 2064 mixin Emits!MouseOutEvent; /// 2065 mixin Emits!MouseEnterEvent; /// 2066 mixin Emits!MouseLeaveEvent; /// 2067 2068 mixin Emits!ResizeEvent; /// 2069 2070 mixin Emits!BlurEvent; /// 2071 mixin Emits!FocusEvent; /// 2072 2073 mixin Emits!FocusInEvent; /// 2074 mixin Emits!FocusOutEvent; /// 2075 } 2076 2077 /+ 2078 /++ 2079 Interface to indicate that the widget has a simple value property. 2080 2081 History: 2082 Added August 26, 2021 2083 +/ 2084 interface HasValue!T { 2085 /// Getter 2086 @property T value(); 2087 /// Setter 2088 @property void value(T); 2089 } 2090 2091 /++ 2092 Interface to indicate that the widget has a range of possible values for its simple value property. 2093 This would be present on something like a slider or possibly a number picker. 2094 2095 History: 2096 Added September 11, 2021 2097 +/ 2098 interface HasRangeOfValues!T : HasValue!T { 2099 /// The minimum and maximum values in the range, inclusive. 2100 @property T minValue(); 2101 @property void minValue(T); /// ditto 2102 @property T maxValue(); /// ditto 2103 @property void maxValue(T); /// ditto 2104 2105 /// The smallest step the user interface allows. User may still type in values without this limitation. 2106 @property void step(T); 2107 @property T step(); /// ditto 2108 } 2109 2110 /++ 2111 Interface to indicate that the widget has a list of possible values the user can choose from. 2112 This would be present on something like a drop-down selector. 2113 2114 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2115 combobox. 2116 2117 History: 2118 Added September 11, 2021 2119 +/ 2120 interface HasListOfValues!T : HasValue!T { 2121 @property T[] values; 2122 @property void values(T[]); 2123 2124 @property int selectedIndex(); // note it may return -1! 2125 @property void selectedIndex(int); 2126 } 2127 +/ 2128 2129 /++ 2130 History: 2131 Added September 2021 (dub v10.4) 2132 +/ 2133 class GridLayout : Layout { 2134 2135 // 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. 2136 2137 /++ 2138 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2139 +/ 2140 enum Gravity { 2141 Center = 0, 2142 NorthWest = North | West, 2143 North = 0b10_00, 2144 NorthEast = North | East, 2145 West = 0b00_10, 2146 East = 0b00_01, 2147 SouthWest = South | West, 2148 South = 0b01_00, 2149 SouthEast = South | East, 2150 } 2151 2152 /++ 2153 The width and height are in some proportional units and can often just be 12. 2154 +/ 2155 this(int width, int height, Widget parent) { 2156 this.gridWidth = width; 2157 this.gridHeight = height; 2158 super(parent); 2159 } 2160 2161 /++ 2162 Sets the position of the given child. 2163 2164 The units of these arguments are in the proportional grid units you set in the constructor. 2165 +/ 2166 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2167 // ensure it is in bounds 2168 // then ensure no overlaps 2169 2170 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2171 2172 foreach(ref position; positions) { 2173 if(position.widget is child) { 2174 position = p; 2175 goto set; 2176 } 2177 } 2178 2179 positions ~= p; 2180 2181 set: 2182 2183 // FIXME: should this batch? 2184 queueRecomputeChildLayout(); 2185 2186 return child; 2187 } 2188 2189 override void addChild(Widget w, int position = int.max) { 2190 super.addChild(w, position); 2191 //positions ~= ChildPosition(w); 2192 if(position != int.max) { 2193 // FIXME: align it so they actually match. 2194 } 2195 } 2196 2197 override void widgetRemoved(size_t idx, Widget w) { 2198 // FIXME: keep the positions array aligned 2199 // positions[idx].widget = null; 2200 } 2201 2202 override void recomputeChildLayout() { 2203 registerMovement(); 2204 int onGrid = cast(int) positions.length; 2205 c: foreach(child; children) { 2206 // just snap it to the grid 2207 if(onGrid) 2208 foreach(position; positions) 2209 if(position.widget is child) { 2210 child.x = this.width * position.x / this.gridWidth; 2211 child.y = this.height * position.y / this.gridHeight; 2212 child.width = this.width * position.width / this.gridWidth; 2213 child.height = this.height * position.height / this.gridHeight; 2214 2215 auto diff = child.width - child.maxWidth(); 2216 // FIXME: gravity? 2217 if(diff > 0) { 2218 child.width = child.width - diff; 2219 2220 if(position.gravity & Gravity.West) { 2221 // nothing needed, already aligned 2222 } else if(position.gravity & Gravity.East) { 2223 child.x += diff; 2224 } else { 2225 child.x += diff / 2; 2226 } 2227 } 2228 2229 diff = child.height - child.maxHeight(); 2230 // FIXME: gravity? 2231 if(diff > 0) { 2232 child.height = child.height - diff; 2233 2234 if(position.gravity & Gravity.North) { 2235 // nothing needed, already aligned 2236 } else if(position.gravity & Gravity.South) { 2237 child.y += diff; 2238 } else { 2239 child.y += diff / 2; 2240 } 2241 } 2242 2243 2244 child.recomputeChildLayout(); 2245 onGrid--; 2246 continue c; 2247 } 2248 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2249 } 2250 } 2251 2252 private struct ChildPosition { 2253 Widget widget; 2254 int x; 2255 int y; 2256 int width; 2257 int height; 2258 Gravity gravity; 2259 } 2260 private ChildPosition[] positions; 2261 2262 int gridWidth = 12; 2263 int gridHeight = 12; 2264 } 2265 2266 /// 2267 abstract class ComboboxBase : Widget { 2268 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2269 // or to always show the list, we want CBS_SIMPLE == 1 2270 version(win32_widgets) 2271 this(uint style, Widget parent) { 2272 super(parent); 2273 createWin32Window(this, "ComboBox"w, null, style); 2274 } 2275 else version(custom_widgets) 2276 this(Widget parent) { 2277 super(parent); 2278 2279 addEventListener((KeyDownEvent event) { 2280 if(event.key == Key.Up) { 2281 if(selection_ > -1) { // -1 means select blank 2282 selection_--; 2283 fireChangeEvent(); 2284 } 2285 event.preventDefault(); 2286 } 2287 if(event.key == Key.Down) { 2288 if(selection_ + 1 < options.length) { 2289 selection_++; 2290 fireChangeEvent(); 2291 } 2292 event.preventDefault(); 2293 } 2294 2295 }); 2296 2297 } 2298 else static assert(false); 2299 2300 /++ 2301 Returns the current list of options in the selection. 2302 2303 History: 2304 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2305 +/ 2306 final @property string[] options() const { 2307 return cast(string[]) options_; 2308 } 2309 2310 private string[] options_; 2311 private int selection_ = -1; 2312 2313 /++ 2314 Adds an option to the end of options array. 2315 +/ 2316 void addOption(string s) { 2317 options_ ~= s; 2318 version(win32_widgets) 2319 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2320 } 2321 2322 /++ 2323 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2324 +/ 2325 int getSelection() { 2326 return selection_; 2327 } 2328 2329 /++ 2330 Returns the current selection as a string. 2331 2332 History: 2333 Added November 17, 2021 2334 +/ 2335 string getSelectionString() { 2336 return selection_ == -1 ? null : options[selection_]; 2337 } 2338 2339 /++ 2340 Sets the current selection to an index in the options array, or to the given option if present. 2341 Please note that the string version may do a linear lookup. 2342 2343 Returns: 2344 the index you passed in 2345 2346 History: 2347 The `string` based overload was added on March 1, 2022 (dub v10.7). 2348 2349 The return value was `void` prior to March 1, 2022. 2350 +/ 2351 int setSelection(int idx) { 2352 selection_ = idx; 2353 version(win32_widgets) 2354 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2355 2356 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2357 t.dispatch(); 2358 2359 return idx; 2360 } 2361 2362 /// ditto 2363 int setSelection(string s) { 2364 if(s !is null) 2365 foreach(idx, item; options) 2366 if(item == s) { 2367 return setSelection(cast(int) idx); 2368 } 2369 return setSelection(-1); 2370 } 2371 2372 /++ 2373 This event is fired when the selection changes. Note it inherits 2374 from ChangeEvent!string, meaning you can use that as well, and it also 2375 fills in [Event.intValue]. 2376 +/ 2377 static class SelectionChangedEvent : ChangeEvent!string { 2378 this(Widget target, int iv, string sv) { 2379 super(target, &stringValue); 2380 this.iv = iv; 2381 this.sv = sv; 2382 } 2383 immutable int iv; 2384 immutable string sv; 2385 2386 override @property string stringValue() { return sv; } 2387 override @property int intValue() { return iv; } 2388 } 2389 2390 version(win32_widgets) 2391 override void handleWmCommand(ushort cmd, ushort id) { 2392 if(cmd == CBN_SELCHANGE) { 2393 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2394 fireChangeEvent(); 2395 } 2396 } 2397 2398 private void fireChangeEvent() { 2399 if(selection_ >= options.length) 2400 selection_ = -1; 2401 2402 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2403 t.dispatch(); 2404 } 2405 2406 version(win32_widgets) { 2407 override int minHeight() { return defaultLineHeight + 6; } 2408 override int maxHeight() { return defaultLineHeight + 6; } 2409 } else { 2410 override int minHeight() { return defaultLineHeight + 4; } 2411 override int maxHeight() { return defaultLineHeight + 4; } 2412 } 2413 2414 version(custom_widgets) { 2415 2416 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2417 2418 SimpleWindow dropDown; 2419 void popup() { 2420 auto w = width; 2421 // FIXME: suggestedDropdownHeight see below 2422 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2423 2424 auto coord = this.globalCoordinates(); 2425 auto dropDown = new SimpleWindow( 2426 w, h, 2427 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2428 2429 dropDown.move(coord.x, coord.y + this.height); 2430 2431 { 2432 auto cs = getComputedStyle(); 2433 auto painter = dropDown.draw(); 2434 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2435 auto p = Point(4, 4); 2436 painter.outlineColor = cs.foregroundColor; 2437 foreach(option; options) { 2438 painter.drawText(p, option); 2439 p.y += defaultLineHeight; 2440 } 2441 } 2442 2443 dropDown.setEventHandlers( 2444 (MouseEvent event) { 2445 if(event.type == MouseEventType.buttonReleased) { 2446 dropDown.close(); 2447 auto element = (event.y - 4) / defaultLineHeight; 2448 if(element >= 0 && element <= options.length) { 2449 selection_ = element; 2450 2451 fireChangeEvent(); 2452 } 2453 } 2454 } 2455 ); 2456 2457 dropDown.visibilityChanged = (bool visible) { 2458 if(visible) { 2459 this.redraw(); 2460 dropDown.grabInput(); 2461 } else { 2462 dropDown.releaseInputGrab(); 2463 } 2464 }; 2465 2466 dropDown.show(); 2467 } 2468 2469 } 2470 } 2471 2472 /++ 2473 A drop-down list where the user must select one of the 2474 given options. Like `<select>` in HTML. 2475 +/ 2476 class DropDownSelection : ComboboxBase { 2477 this(Widget parent) { 2478 version(win32_widgets) 2479 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2480 else version(custom_widgets) { 2481 super(parent); 2482 2483 addEventListener("focus", () { this.redraw; }); 2484 addEventListener("blur", () { this.redraw; }); 2485 addEventListener(EventType.change, () { this.redraw; }); 2486 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2487 addEventListener((KeyDownEvent event) { 2488 if(event.key == Key.Space) 2489 popup(); 2490 }); 2491 } else static assert(false); 2492 } 2493 2494 mixin Padding!q{2}; 2495 static class Style : Widget.Style { 2496 override FrameStyle borderStyle() { return FrameStyle.risen; } 2497 } 2498 mixin OverrideStyle!Style; 2499 2500 version(custom_widgets) 2501 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2502 auto cs = getComputedStyle(); 2503 2504 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2505 2506 painter.outlineColor = cs.foregroundColor; 2507 painter.fillColor = cs.foregroundColor; 2508 2509 /+ 2510 Point[4] triangle; 2511 enum padding = 6; 2512 enum paddingV = 7; 2513 enum triangleWidth = 10; 2514 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2515 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2516 triangle[2] = Point(width - padding - 0, paddingV); 2517 triangle[3] = triangle[0]; 2518 painter.drawPolygon(triangle[]); 2519 +/ 2520 2521 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2522 2523 painter.drawPolygon( 2524 scaleWithDpi(Point(2, 6) + offset), 2525 scaleWithDpi(Point(7, 11) + offset), 2526 scaleWithDpi(Point(12, 6) + offset), 2527 scaleWithDpi(Point(2, 6) + offset) 2528 ); 2529 2530 2531 return bounds; 2532 } 2533 2534 version(win32_widgets) 2535 override void registerMovement() { 2536 version(win32_widgets) { 2537 if(hwnd) { 2538 auto pos = getChildPositionRelativeToParentHwnd(this); 2539 // the height given to this from Windows' perspective is supposed 2540 // to include the drop down's height. so I add to it to give some 2541 // room for that. 2542 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2543 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2544 } 2545 } 2546 sendResizeEvent(); 2547 } 2548 } 2549 2550 /++ 2551 A text box with a drop down arrow listing selections. 2552 The user can choose from the list, or type their own. 2553 +/ 2554 class FreeEntrySelection : ComboboxBase { 2555 this(Widget parent) { 2556 version(win32_widgets) 2557 super(2 /* CBS_DROPDOWN */, parent); 2558 else version(custom_widgets) { 2559 super(parent); 2560 auto hl = new HorizontalLayout(this); 2561 lineEdit = new LineEdit(hl); 2562 2563 tabStop = false; 2564 2565 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2566 2567 auto btn = new class ArrowButton { 2568 this() { 2569 super(ArrowDirection.down, hl); 2570 } 2571 override int maxHeight() { 2572 return lineEdit.maxHeight; 2573 } 2574 }; 2575 //btn.addDirectEventListener("focus", &lineEdit.focus); 2576 btn.addEventListener("triggered", &this.popup); 2577 addEventListener(EventType.change, (Event event) { 2578 lineEdit.content = event.stringValue; 2579 lineEdit.focus(); 2580 redraw(); 2581 }); 2582 } 2583 else static assert(false); 2584 } 2585 2586 version(custom_widgets) { 2587 LineEdit lineEdit; 2588 } 2589 } 2590 2591 /++ 2592 A combination of free entry with a list below it. 2593 +/ 2594 class ComboBox : ComboboxBase { 2595 this(Widget parent) { 2596 version(win32_widgets) 2597 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2598 else version(custom_widgets) { 2599 super(parent); 2600 lineEdit = new LineEdit(this); 2601 listWidget = new ListWidget(this); 2602 listWidget.multiSelect = false; 2603 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2604 string c = null; 2605 foreach(option; listWidget.options) 2606 if(option.selected) { 2607 c = option.label; 2608 break; 2609 } 2610 lineEdit.content = c; 2611 }); 2612 2613 listWidget.tabStop = false; 2614 this.tabStop = false; 2615 listWidget.addEventListener("focus", &lineEdit.focus); 2616 this.addEventListener("focus", &lineEdit.focus); 2617 2618 addDirectEventListener(EventType.change, { 2619 listWidget.setSelection(selection_); 2620 if(selection_ != -1) 2621 lineEdit.content = options[selection_]; 2622 lineEdit.focus(); 2623 redraw(); 2624 }); 2625 2626 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2627 2628 listWidget.addDirectEventListener(EventType.change, { 2629 int set = -1; 2630 foreach(idx, opt; listWidget.options) 2631 if(opt.selected) { 2632 set = cast(int) idx; 2633 break; 2634 } 2635 if(set != selection_) 2636 this.setSelection(set); 2637 }); 2638 } else static assert(false); 2639 } 2640 2641 override int minHeight() { return defaultLineHeight * 3; } 2642 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2643 override int heightStretchiness() { return 5; } 2644 2645 version(custom_widgets) { 2646 LineEdit lineEdit; 2647 ListWidget listWidget; 2648 2649 override void addOption(string s) { 2650 listWidget.options ~= ListWidget.Option(s); 2651 ComboboxBase.addOption(s); 2652 } 2653 } 2654 } 2655 2656 /+ 2657 class Spinner : Widget { 2658 version(win32_widgets) 2659 this(Widget parent) { 2660 super(parent); 2661 parentWindow = parent.parentWindow; 2662 auto hlayout = new HorizontalLayout(this); 2663 lineEdit = new LineEdit(hlayout); 2664 upDownControl = new UpDownControl(hlayout); 2665 } 2666 2667 LineEdit lineEdit; 2668 UpDownControl upDownControl; 2669 } 2670 2671 class UpDownControl : Widget { 2672 version(win32_widgets) 2673 this(Widget parent) { 2674 super(parent); 2675 parentWindow = parent.parentWindow; 2676 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2677 } 2678 2679 override int minHeight() { return defaultLineHeight; } 2680 override int maxHeight() { return defaultLineHeight * 3/2; } 2681 2682 override int minWidth() { return defaultLineHeight * 3/2; } 2683 override int maxWidth() { return defaultLineHeight * 3/2; } 2684 } 2685 +/ 2686 2687 /+ 2688 class DataView : Widget { 2689 // this is the omnibus data viewer 2690 // the internal data layout is something like: 2691 // string[string][] but also each node can have parents 2692 } 2693 +/ 2694 2695 2696 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2697 2698 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2699 2700 // FIXME: menus should prolly capture the mouse. ugh i kno. 2701 /* 2702 TextEdit needs: 2703 2704 * caret manipulation 2705 * selection control 2706 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2707 2708 For example: 2709 2710 connect(paste, &textEdit.insertTextAtCaret); 2711 2712 would be nice. 2713 2714 2715 2716 I kinda want an omnibus dataview that combines list, tree, 2717 and table - it can be switched dynamically between them. 2718 2719 Flattening policy: only show top level, show recursive, show grouped 2720 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2721 2722 Single select, multi select, organization, drag+drop 2723 */ 2724 2725 //static if(UsingSimpledisplayX11) 2726 version(win32_widgets) {} 2727 else version(custom_widgets) { 2728 enum scrollClickRepeatInterval = 50; 2729 2730 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2731 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2732 enum activeTabColor = lightAccentColor; 2733 enum hoveringColor = Color(228, 228, 228); 2734 enum buttonColor = windowBackgroundColor; 2735 enum depressedButtonColor = darkAccentColor; 2736 enum activeListXorColor = Color(255, 255, 127); 2737 enum progressBarColor = Color(0, 0, 128); 2738 enum activeMenuItemColor = Color(0, 0, 128); 2739 2740 }} 2741 else static assert(false); 2742 deprecated("Get these properties off the `visualTheme` instead.") { 2743 // these are used by horizontal rule so not just custom_widgets. for now at least. 2744 enum darkAccentColor = Color(172, 172, 172); 2745 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2746 } 2747 2748 private const(wchar)* toWstringzInternal(in char[] s) { 2749 wchar[] str; 2750 str.reserve(s.length + 1); 2751 foreach(dchar ch; s) 2752 str ~= ch; 2753 str ~= '\0'; 2754 return str.ptr; 2755 } 2756 2757 static if(SimpledisplayTimerAvailable) 2758 void setClickRepeat(Widget w, int interval, int delay = 250) { 2759 Timer timer; 2760 int delayRemaining = delay / interval; 2761 if(delayRemaining <= 1) 2762 delayRemaining = 2; 2763 2764 immutable originalDelayRemaining = delayRemaining; 2765 2766 w.addDirectEventListener((scope MouseDownEvent ev) { 2767 if(ev.srcElement !is w) 2768 return; 2769 if(timer !is null) { 2770 timer.destroy(); 2771 timer = null; 2772 } 2773 delayRemaining = originalDelayRemaining; 2774 timer = new Timer(interval, () { 2775 if(delayRemaining > 0) 2776 delayRemaining--; 2777 else { 2778 auto ev = new Event("triggered", w); 2779 ev.sendDirectly(); 2780 } 2781 }); 2782 }); 2783 2784 w.addDirectEventListener((scope MouseUpEvent ev) { 2785 if(ev.srcElement !is w) 2786 return; 2787 if(timer !is null) { 2788 timer.destroy(); 2789 timer = null; 2790 } 2791 }); 2792 2793 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2794 if(ev.srcElement !is w) 2795 return; 2796 if(timer !is null) { 2797 timer.destroy(); 2798 timer = null; 2799 } 2800 }); 2801 2802 } 2803 else 2804 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2805 2806 enum FrameStyle { 2807 none, /// 2808 risen, /// a 3d pop-out effect (think Windows 95 button) 2809 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2810 solid, /// 2811 dotted, /// 2812 fantasy, /// a style based on a popular fantasy video game 2813 } 2814 2815 version(custom_widgets) 2816 deprecated 2817 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2818 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2819 } 2820 2821 version(custom_widgets) 2822 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2823 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2824 } 2825 2826 version(custom_widgets) 2827 deprecated 2828 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2829 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2830 } 2831 2832 int getBorderWidth(FrameStyle style) { 2833 final switch(style) { 2834 case FrameStyle.sunk, FrameStyle.risen: 2835 return 2; 2836 case FrameStyle.none: 2837 return 0; 2838 case FrameStyle.solid: 2839 return 1; 2840 case FrameStyle.dotted: 2841 return 1; 2842 case FrameStyle.fantasy: 2843 return 3; 2844 } 2845 } 2846 2847 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2848 int borderWidth = getBorderWidth(style); 2849 final switch(style) { 2850 case FrameStyle.sunk, FrameStyle.risen: 2851 // outer layer 2852 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2853 break; 2854 case FrameStyle.none: 2855 painter.outlineColor = background; 2856 break; 2857 case FrameStyle.solid: 2858 painter.pen = Pen(border, 1); 2859 break; 2860 case FrameStyle.dotted: 2861 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2862 break; 2863 case FrameStyle.fantasy: 2864 painter.pen = Pen(border, 3); 2865 break; 2866 } 2867 2868 painter.fillColor = background; 2869 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2870 2871 2872 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2873 // 3d effect 2874 auto vt = WidgetPainter.visualTheme; 2875 2876 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2877 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2878 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2879 2880 // inner layer 2881 //right, bottom 2882 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2883 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2884 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2885 // left, top 2886 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2887 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2888 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2889 } else if(style == FrameStyle.fantasy) { 2890 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2891 painter.fillColor = Color.transparent; 2892 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2893 } 2894 2895 return borderWidth; 2896 } 2897 2898 /++ 2899 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. 2900 2901 See_Also: 2902 [MenuItem] 2903 [ToolButton] 2904 [Menu.addItem] 2905 +/ 2906 class Action { 2907 version(win32_widgets) { 2908 private int id; 2909 private static int lastId = 9000; 2910 private static Action[int] mapping; 2911 } 2912 2913 KeyEvent accelerator; 2914 2915 // FIXME: disable message 2916 // and toggle thing? 2917 // ??? and trigger arguments too ??? 2918 2919 /++ 2920 Params: 2921 label = the textual label 2922 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2923 triggered = initial handler, more can be added via the [triggered] member. 2924 +/ 2925 this(string label, ushort icon = 0, void delegate() triggered = null) { 2926 this.label = label; 2927 this.iconId = icon; 2928 if(triggered !is null) 2929 this.triggered ~= triggered; 2930 version(win32_widgets) { 2931 id = ++lastId; 2932 mapping[id] = this; 2933 } 2934 } 2935 2936 private string label; 2937 private ushort iconId; 2938 // icon 2939 2940 // when it is triggered, the triggered event is fired on the window 2941 /// The list of handlers when it is triggered. 2942 void delegate()[] triggered; 2943 } 2944 2945 /* 2946 plan: 2947 keyboard accelerators 2948 2949 * menus (and popups and tooltips) 2950 * status bar 2951 * toolbars and buttons 2952 2953 sortable table view 2954 2955 maybe notification area icons 2956 basic clipboard 2957 2958 * radio box 2959 splitter 2960 toggle buttons (optionally mutually exclusive, like in Paint) 2961 label, rich text display, multi line plain text (selectable) 2962 * fieldset 2963 * nestable grid layout 2964 single line text input 2965 * multi line text input 2966 slider 2967 spinner 2968 list box 2969 drop down 2970 combo box 2971 auto complete box 2972 * progress bar 2973 2974 terminal window/widget (on unix it might even be a pty but really idk) 2975 2976 ok button 2977 cancel button 2978 2979 keyboard hotkeys 2980 2981 scroll widget 2982 2983 event redirections and network transparency 2984 script integration 2985 */ 2986 2987 2988 /* 2989 MENUS 2990 2991 auto bar = new MenuBar(window); 2992 window.menuBar = bar; 2993 2994 auto fileMenu = bar.addItem(new Menu("&File")); 2995 fileMenu.addItem(new MenuItem("&Exit")); 2996 2997 2998 EVENTS 2999 3000 For controls, you should usually use "triggered" rather than "click", etc., because 3001 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3002 This is the case on menus and pushbuttons. 3003 3004 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3005 */ 3006 3007 3008 /* 3009 enum LinePreference { 3010 AlwaysOnOwnLine, // always on its own line 3011 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3012 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 3013 } 3014 */ 3015 3016 /++ 3017 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. 3018 3019 --- 3020 class MyWidget : Widget { 3021 this(Widget parent) { super(parent); } 3022 3023 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3024 mixin Padding!q{4}; 3025 3026 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3027 mixin Margin!q{8}; 3028 3029 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3030 // while Top/Bottom/Right remain 8 from the mixin above. 3031 override int marginLeft() { return 2; } 3032 } 3033 --- 3034 3035 3036 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]). 3037 3038 Padding is the area inside a widget where its background is drawn, but the content avoids. 3039 3040 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!). 3041 3042 * 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. 3043 +/ 3044 mixin template Padding(string code) { 3045 override int paddingLeft() { return mixin(code);} 3046 override int paddingRight() { return mixin(code);} 3047 override int paddingTop() { return mixin(code);} 3048 override int paddingBottom() { return mixin(code);} 3049 } 3050 3051 /// ditto 3052 mixin template Margin(string code) { 3053 override int marginLeft() { return mixin(code);} 3054 override int marginRight() { return mixin(code);} 3055 override int marginTop() { return mixin(code);} 3056 override int marginBottom() { return mixin(code);} 3057 } 3058 3059 private 3060 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3061 enum calcingV = relevantMeasure == "height"; 3062 3063 parent.registerMovement(); 3064 3065 if(parent.children.length == 0) 3066 return; 3067 3068 auto parentStyle = parent.getComputedStyle(); 3069 3070 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3071 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3072 3073 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3074 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3075 3076 // my own width and height should already be set by the caller of this function... 3077 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3078 mixin("parentStyle.padding"~firstThingy~"()") - 3079 mixin("parentStyle.padding"~secondThingy~"()"); 3080 3081 int stretchinessSum; 3082 int stretchyChildSum; 3083 int lastMargin = 0; 3084 3085 int shrinkinessSum; 3086 int shrinkyChildSum; 3087 3088 // set initial size 3089 foreach(child; parent.children) { 3090 3091 auto childStyle = child.getComputedStyle(); 3092 3093 if(cast(StaticPosition) child) 3094 continue; 3095 if(child.hidden) 3096 continue; 3097 3098 const iw = child.flexBasisWidth(); 3099 const ih = child.flexBasisHeight(); 3100 3101 static if(calcingV) { 3102 child.width = parent.width - 3103 mixin("childStyle.margin"~otherFirstThingy~"()") - 3104 mixin("childStyle.margin"~otherSecondThingy~"()") - 3105 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3106 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3107 3108 if(child.width < 0) 3109 child.width = 0; 3110 if(child.width > childStyle.maxWidth()) 3111 child.width = childStyle.maxWidth(); 3112 3113 if(iw > 0) { 3114 auto totalPossible = child.width; 3115 if(child.width > iw && child.widthStretchiness() == 0) 3116 child.width = iw; 3117 } 3118 3119 child.height = mymax(childStyle.minHeight(), ih); 3120 } else { 3121 // set to take all the space 3122 child.height = parent.height - 3123 mixin("childStyle.margin"~firstThingy~"()") - 3124 mixin("childStyle.margin"~secondThingy~"()") - 3125 mixin("parentStyle.padding"~firstThingy~"()") - 3126 mixin("parentStyle.padding"~secondThingy~"()"); 3127 3128 // then clamp it 3129 if(child.height < 0) 3130 child.height = 0; 3131 if(child.height > childStyle.maxHeight()) 3132 child.height = childStyle.maxHeight(); 3133 3134 // and if possible, respect the ideal target 3135 if(ih > 0) { 3136 auto totalPossible = child.height; 3137 if(child.height > ih && child.heightStretchiness() == 0) 3138 child.height = ih; 3139 } 3140 3141 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3142 child.width = mymax(childStyle.minWidth(), iw); 3143 } 3144 3145 spaceRemaining -= mixin("child." ~ relevantMeasure); 3146 3147 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3148 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3149 lastMargin = margin; 3150 spaceRemaining -= thisMargin + margin; 3151 3152 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3153 stretchinessSum += s; 3154 if(s > 0) 3155 stretchyChildSum++; 3156 3157 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3158 shrinkinessSum += s2; 3159 if(s2 > 0) 3160 shrinkyChildSum++; 3161 } 3162 3163 if(spaceRemaining < 0 && shrinkyChildSum) { 3164 // shrink to get into the space if it is possible 3165 auto toRemove = -spaceRemaining; 3166 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3167 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3168 3169 // FIXME: wtf why am i shrinking things with no shrinkiness? 3170 3171 foreach(child; parent.children) { 3172 auto childStyle = child.getComputedStyle(); 3173 if(cast(StaticPosition) child) 3174 continue; 3175 if(child.hidden) 3176 continue; 3177 static if(calcingV) { 3178 auto maximum = childStyle.maxHeight(); 3179 } else { 3180 auto maximum = childStyle.maxWidth(); 3181 } 3182 3183 if(mixin("child._" ~ relevantMeasure) >= maximum) 3184 continue; 3185 3186 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3187 3188 spaceRemaining += removalPerItem + remainder; 3189 } 3190 } 3191 3192 // stretch to fill space 3193 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3194 auto spacePerChild = spaceRemaining / stretchinessSum; 3195 bool spreadEvenly; 3196 bool giveToBiggest; 3197 if(spacePerChild <= 0) { 3198 spacePerChild = spaceRemaining / stretchyChildSum; 3199 spreadEvenly = true; 3200 } 3201 if(spacePerChild <= 0) { 3202 giveToBiggest = true; 3203 } 3204 int previousSpaceRemaining = spaceRemaining; 3205 stretchinessSum = 0; 3206 Widget mostStretchy; 3207 int mostStretchyS; 3208 foreach(child; parent.children) { 3209 auto childStyle = child.getComputedStyle(); 3210 if(cast(StaticPosition) child) 3211 continue; 3212 if(child.hidden) 3213 continue; 3214 static if(calcingV) { 3215 auto maximum = childStyle.maxHeight(); 3216 } else { 3217 auto maximum = childStyle.maxWidth(); 3218 } 3219 3220 if(mixin("child." ~ relevantMeasure) >= maximum) { 3221 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3222 mixin("child._" ~ relevantMeasure) -= adj; 3223 spaceRemaining += adj; 3224 continue; 3225 } 3226 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3227 if(s <= 0) 3228 continue; 3229 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3230 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3231 spaceRemaining -= spaceAdjustment; 3232 if(mixin("child." ~ relevantMeasure) > maximum) { 3233 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3234 mixin("child._" ~ relevantMeasure) -= diff; 3235 spaceRemaining += diff; 3236 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3237 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3238 if(mostStretchy is null || s >= mostStretchyS) { 3239 mostStretchy = child; 3240 mostStretchyS = s; 3241 } 3242 } 3243 } 3244 3245 if(giveToBiggest && mostStretchy !is null) { 3246 auto child = mostStretchy; 3247 auto childStyle = child.getComputedStyle(); 3248 int spaceAdjustment = spaceRemaining; 3249 3250 static if(calcingV) 3251 auto maximum = childStyle.maxHeight(); 3252 else 3253 auto maximum = childStyle.maxWidth(); 3254 3255 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3256 spaceRemaining -= spaceAdjustment; 3257 if(mixin("child._" ~ relevantMeasure) > maximum) { 3258 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3259 mixin("child._" ~ relevantMeasure) -= diff; 3260 spaceRemaining += diff; 3261 } 3262 } 3263 3264 if(spaceRemaining == previousSpaceRemaining) { 3265 if(mostStretchy !is null) { 3266 static if(calcingV) 3267 auto maximum = mostStretchy.maxHeight(); 3268 else 3269 auto maximum = mostStretchy.maxWidth(); 3270 3271 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3272 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3273 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3274 } 3275 break; // apparently nothing more we can do 3276 } 3277 } 3278 3279 foreach(child; parent.children) { 3280 auto childStyle = child.getComputedStyle(); 3281 if(cast(StaticPosition) child) 3282 continue; 3283 if(child.hidden) 3284 continue; 3285 3286 static if(calcingV) 3287 auto maximum = childStyle.maxHeight(); 3288 else 3289 auto maximum = childStyle.maxWidth(); 3290 if(mixin("child._" ~ relevantMeasure) > maximum) 3291 mixin("child._" ~ relevantMeasure) = maximum; 3292 } 3293 3294 // position 3295 lastMargin = 0; 3296 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3297 foreach(child; parent.children) { 3298 auto childStyle = child.getComputedStyle(); 3299 if(cast(StaticPosition) child) { 3300 child.recomputeChildLayout(); 3301 continue; 3302 } 3303 if(child.hidden) 3304 continue; 3305 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3306 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3307 currentPos += thisMargin; 3308 static if(calcingV) { 3309 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3310 child.y = currentPos; 3311 } else { 3312 child.x = currentPos; 3313 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3314 3315 } 3316 currentPos += mixin("child." ~ relevantMeasure); 3317 currentPos += margin; 3318 lastMargin = margin; 3319 3320 child.recomputeChildLayout(); 3321 } 3322 } 3323 3324 int mymax(int a, int b) { return a > b ? a : b; } 3325 int mymax(int a, int b, int c) { 3326 auto d = mymax(a, b); 3327 return c > d ? c : d; 3328 } 3329 3330 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3331 // and here, it must be integrable with the layout, the event system, and not be painted over. 3332 version(win32_widgets) { 3333 3334 // this function just does stuff that a parent window needs for redirection 3335 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3336 this_.hookedWndProc(msg, wParam, lParam); 3337 3338 switch(msg) { 3339 3340 case WM_VSCROLL, WM_HSCROLL: 3341 auto pos = HIWORD(wParam); 3342 auto m = LOWORD(wParam); 3343 3344 auto scrollbarHwnd = cast(HWND) lParam; 3345 3346 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3347 3348 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3349 3350 switch(m) { 3351 /+ 3352 // I don't think those messages are ever actually sent normally by the widget itself, 3353 // they are more used for the keyboard interface. methinks. 3354 case SB_BOTTOM: 3355 // writeln("end"); 3356 auto event = new Event("scrolltoend", *widgetp); 3357 event.dispatch(); 3358 //if(!event.defaultPrevented) 3359 break; 3360 case SB_TOP: 3361 // writeln("top"); 3362 auto event = new Event("scrolltobeginning", *widgetp); 3363 event.dispatch(); 3364 break; 3365 case SB_ENDSCROLL: 3366 // idk 3367 break; 3368 +/ 3369 case SB_LINEDOWN: 3370 (*widgetp).emitCommand!"scrolltonextline"(); 3371 return 0; 3372 case SB_LINEUP: 3373 (*widgetp).emitCommand!"scrolltopreviousline"(); 3374 return 0; 3375 case SB_PAGEDOWN: 3376 (*widgetp).emitCommand!"scrolltonextpage"(); 3377 return 0; 3378 case SB_PAGEUP: 3379 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3380 return 0; 3381 case SB_THUMBPOSITION: 3382 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3383 ev.dispatch(); 3384 return 0; 3385 case SB_THUMBTRACK: 3386 // eh kinda lying but i like the real time update display 3387 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3388 ev.dispatch(); 3389 3390 // the event loop doesn't seem to carry on with a requested redraw.. 3391 // so we request it to get our dirty bit set... 3392 // then we need to immediately actually redraw it too for instant feedback to user 3393 SimpleWindow.processAllCustomEvents(); 3394 SimpleWindow.processAllCustomEvents(); 3395 //if(this_.parentWindow) 3396 //this_.parentWindow.actualRedraw(); 3397 3398 // and this ensures the WM_PAINT message is sent fairly quickly 3399 // still seems to lag a little in large windows but meh it basically works. 3400 if(this_.parentWindow) { 3401 // FIXME: if painting is slow, this does still lag 3402 // we probably will want to expose some user hook to ScrollWindowEx 3403 // or something. 3404 UpdateWindow(this_.parentWindow.hwnd); 3405 } 3406 return 0; 3407 default: 3408 } 3409 } 3410 break; 3411 3412 case WM_CONTEXTMENU: 3413 auto hwndFrom = cast(HWND) wParam; 3414 3415 auto xPos = cast(short) LOWORD(lParam); 3416 auto yPos = cast(short) HIWORD(lParam); 3417 3418 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3419 POINT p; 3420 p.x = xPos; 3421 p.y = yPos; 3422 ScreenToClient(hwnd, &p); 3423 auto clientX = cast(ushort) p.x; 3424 auto clientY = cast(ushort) p.y; 3425 3426 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3427 3428 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3429 return 0; 3430 } 3431 } 3432 break; 3433 3434 case WM_DRAWITEM: 3435 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3436 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3437 return (*widgetp).handleWmDrawItem(dis); 3438 } 3439 break; 3440 3441 case WM_NOTIFY: 3442 auto hdr = cast(NMHDR*) lParam; 3443 auto hwndFrom = hdr.hwndFrom; 3444 auto code = hdr.code; 3445 3446 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3447 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3448 } 3449 break; 3450 case WM_COMMAND: 3451 auto handle = cast(HWND) lParam; 3452 auto cmd = HIWORD(wParam); 3453 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3454 3455 default: 3456 // pass it on 3457 } 3458 return 0; 3459 } 3460 3461 3462 3463 extern(Windows) 3464 private 3465 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3466 // but can i merge them?! 3467 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3468 // try { writeln(iMessage); } catch(Exception e) {}; 3469 3470 if(auto te = hWnd in Widget.nativeMapping) { 3471 try { 3472 3473 te.hookedWndProc(iMessage, wParam, lParam); 3474 3475 int mustReturn; 3476 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3477 if(mustReturn) 3478 return ret; 3479 3480 if(iMessage == WM_SETFOCUS) { 3481 auto lol = *te; 3482 while(lol !is null && lol.implicitlyCreated) 3483 lol = lol.parent; 3484 lol.focus(); 3485 //(*te).parentWindow.focusedWidget = lol; 3486 } 3487 3488 3489 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3490 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3491 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3492 //GetStockObject(NULL_BRUSH); 3493 } 3494 3495 auto pos = getChildPositionRelativeToParentOrigin(*te); 3496 lastDefaultPrevented = false; 3497 // try { writeln(typeid(*te)); } catch(Exception e) {} 3498 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3499 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3500 else { 3501 // it was something we recognized, should only call the window procedure if the default was not prevented 3502 } 3503 } catch(Exception e) { 3504 assert(0, e.toString()); 3505 } 3506 return 0; 3507 } 3508 assert(0, "shouldn't be receiving messages for this window...."); 3509 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3510 } 3511 3512 extern(Windows) 3513 private 3514 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3515 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3516 if(iMessage == WM_ERASEBKGND) { 3517 auto dc = GetDC(hWnd); 3518 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3519 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3520 RECT r; 3521 GetWindowRect(hWnd, &r); 3522 // since the pen is null, to fill the whole space, we need the +1 on both. 3523 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3524 SelectObject(dc, p); 3525 SelectObject(dc, b); 3526 ReleaseDC(hWnd, dc); 3527 InvalidateRect(hWnd, null, false); // redraw the border 3528 return 1; 3529 } 3530 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3531 } 3532 3533 /++ 3534 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3535 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3536 of minigui's expectations. 3537 3538 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3539 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3540 3541 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3542 3543 To check if you can use this, use `static if(UsingWin32Widgets)`. 3544 +/ 3545 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3546 assert(p.parentWindow !is null); 3547 assert(p.parentWindow.win.impl.hwnd !is null); 3548 3549 auto bsgroupbox = style == BS_GROUPBOX; 3550 3551 HWND phwnd; 3552 3553 auto wtf = p.parent; 3554 while(wtf) { 3555 if(wtf.hwnd !is null) { 3556 phwnd = wtf.hwnd; 3557 break; 3558 } 3559 wtf = wtf.parent; 3560 } 3561 3562 if(phwnd is null) 3563 phwnd = p.parentWindow.win.impl.hwnd; 3564 3565 assert(phwnd !is null); 3566 3567 WCharzBuffer wt = WCharzBuffer(windowText); 3568 3569 style |= WS_VISIBLE | WS_CHILD; 3570 //if(className != WC_TABCONTROL) 3571 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3572 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3573 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3574 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3575 3576 assert(p.hwnd !is null); 3577 3578 3579 static HFONT font; 3580 if(font is null) { 3581 NONCLIENTMETRICS params; 3582 params.cbSize = params.sizeof; 3583 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3584 font = CreateFontIndirect(¶ms.lfMessageFont); 3585 } 3586 } 3587 3588 if(font) 3589 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3590 3591 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3592 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3593 Widget.nativeMapping[p.hwnd] = p; 3594 3595 if(bsgroupbox) 3596 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3597 else 3598 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3599 3600 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3601 3602 p.registerMovement(); 3603 } 3604 } 3605 3606 version(win32_widgets) 3607 private 3608 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3609 if(hwnd is null || hwnd in Widget.nativeMapping) 3610 return true; 3611 auto parent = cast(Widget) cast(void*) lparam; 3612 Widget p = new Widget(null); 3613 p._parent = parent; 3614 p.parentWindow = parent.parentWindow; 3615 p.hwnd = hwnd; 3616 p.implicitlyCreated = true; 3617 Widget.nativeMapping[p.hwnd] = p; 3618 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3619 return true; 3620 } 3621 3622 /++ 3623 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3624 +/ 3625 struct WidgetPainter { 3626 this(ScreenPainter screenPainter, Widget drawingUpon) { 3627 this.drawingUpon = drawingUpon; 3628 this.screenPainter = screenPainter; 3629 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3630 this.screenPainter.setFont(font); 3631 } 3632 3633 /++ 3634 EXPERIMENTAL. subject to change. 3635 3636 When you draw a cursor, you can draw this to notify your window of where it is, 3637 for IME systems to use. 3638 +/ 3639 void notifyCursorPosition(int x, int y, int width, int height) { 3640 if(auto a = drawingUpon.parentWindow) 3641 if(auto w = a.inputProxy) { 3642 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3643 } 3644 } 3645 3646 3647 /// 3648 ScreenPainter screenPainter; 3649 /// Forward to the screen painter for other methods 3650 alias screenPainter this; 3651 3652 private Widget drawingUpon; 3653 3654 /++ 3655 This is the list of rectangles that actually need to be redrawn. 3656 3657 Not actually implemented yet. 3658 +/ 3659 Rectangle[] invalidatedRectangles; 3660 3661 private static BaseVisualTheme _visualTheme; 3662 3663 /++ 3664 Functions to access the visual theme and helpers to easily use it. 3665 3666 These are aware of the current widget's computed style out of the theme. 3667 +/ 3668 static @property BaseVisualTheme visualTheme() { 3669 if(_visualTheme is null) 3670 _visualTheme = new DefaultVisualTheme(); 3671 return _visualTheme; 3672 } 3673 3674 /// ditto 3675 static @property void visualTheme(BaseVisualTheme theme) { 3676 _visualTheme = theme; 3677 3678 // FIXME: notify all windows about the new theme 3679 } 3680 3681 /// ditto 3682 Color themeForeground() { 3683 return drawingUpon.getComputedStyle().foregroundColor(); 3684 } 3685 3686 /// ditto 3687 Color themeBackground() { 3688 return drawingUpon.getComputedStyle().background.color; 3689 } 3690 3691 int isDarkTheme() { 3692 return 0; // unspecified, yes, no as enum. FIXME 3693 } 3694 3695 /++ 3696 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. 3697 3698 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3699 3700 If you change teh clip rectangle, you should change it back before you return. 3701 3702 3703 The sequence it uses is: 3704 background 3705 content (delegated to you) 3706 border 3707 focused outline 3708 selected overlay 3709 3710 Example code: 3711 3712 --- 3713 void paint(WidgetPainter painter) { 3714 painter.drawThemed((bounds) { 3715 return bounds; // if the selection overlay should be contained, you can return it here. 3716 }); 3717 } 3718 --- 3719 +/ 3720 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3721 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3722 return drawBody(bounds); 3723 }); 3724 } 3725 // this overload is actually mroe for setting the delegate to a virtual function 3726 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3727 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3728 3729 auto cs = drawingUpon.getComputedStyle(); 3730 3731 auto bg = cs.background.color; 3732 3733 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3734 3735 rect.left += borderWidth; 3736 rect.right -= borderWidth; 3737 rect.top += borderWidth; 3738 rect.bottom -= borderWidth; 3739 3740 auto insideBorderRect = rect; 3741 3742 rect.left += cs.paddingLeft; 3743 rect.right -= cs.paddingRight; 3744 rect.top += cs.paddingTop; 3745 rect.bottom -= cs.paddingBottom; 3746 3747 this.outlineColor = this.themeForeground; 3748 this.fillColor = bg; 3749 3750 auto widgetFont = cs.fontCached; 3751 if(widgetFont !is null) 3752 this.setFont(widgetFont); 3753 3754 rect = drawBody(this, rect); 3755 3756 if(widgetFont !is null) { 3757 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3758 this.setFont(vtFont); 3759 else 3760 this.setFont(null); 3761 } 3762 3763 if(auto os = cs.outlineStyle()) { 3764 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3765 this.fillColor = Color.transparent; 3766 this.drawRectangle(insideBorderRect); 3767 } 3768 } 3769 3770 /++ 3771 First, draw the background. 3772 Then draw your content. 3773 Next, draw the border. 3774 And the focused indicator. 3775 And the is-selected box. 3776 3777 If it is focused i can draw the outline too... 3778 3779 If selected i can even do the xor action but that's at the end. 3780 +/ 3781 void drawThemeBackground() { 3782 3783 } 3784 3785 void drawThemeBorder() { 3786 3787 } 3788 3789 // all this stuff is a dangerous experiment.... 3790 static class ScriptableVersion { 3791 ScreenPainterImplementation* p; 3792 int originX, originY; 3793 3794 @scriptable: 3795 void drawRectangle(int x, int y, int width, int height) { 3796 p.drawRectangle(x + originX, y + originY, width, height); 3797 } 3798 void drawLine(int x1, int y1, int x2, int y2) { 3799 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3800 } 3801 void drawText(int x, int y, string text) { 3802 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3803 } 3804 void setOutlineColor(int r, int g, int b) { 3805 p.pen = Pen(Color(r,g,b), 1); 3806 } 3807 void setFillColor(int r, int g, int b) { 3808 p.fillColor = Color(r,g,b); 3809 } 3810 } 3811 3812 ScriptableVersion toArsdJsvar() { 3813 auto sv = new ScriptableVersion; 3814 sv.p = this.screenPainter.impl; 3815 sv.originX = this.screenPainter.originX; 3816 sv.originY = this.screenPainter.originY; 3817 return sv; 3818 } 3819 3820 static WidgetPainter fromJsVar(T)(T t) { 3821 return WidgetPainter.init; 3822 } 3823 // done.......... 3824 } 3825 3826 3827 struct Style { 3828 static struct helper(string m, T) { 3829 enum method = m; 3830 T v; 3831 3832 mixin template MethodOverride(typeof(this) v) { 3833 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3834 } 3835 } 3836 3837 static auto opDispatch(string method, T)(T value) { 3838 return helper!(method, T)(value); 3839 } 3840 } 3841 3842 /++ 3843 Implementation detail of the [ControlledBy] UDA. 3844 3845 History: 3846 Added Oct 28, 2020 3847 +/ 3848 struct ControlledBy_(T, Args...) { 3849 Args args; 3850 3851 static if(Args.length) 3852 this(Args args) { 3853 this.args = args; 3854 } 3855 3856 private T construct(Widget parent) { 3857 return new T(args, parent); 3858 } 3859 } 3860 3861 /++ 3862 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3863 3864 History: 3865 Added Oct 28, 2020 3866 +/ 3867 auto ControlledBy(T, Args...)(Args args) { 3868 return ControlledBy_!(T, Args)(args); 3869 } 3870 3871 struct ContainerMeta { 3872 string name; 3873 ContainerMeta[] children; 3874 Widget function(Widget parent) factory; 3875 3876 Widget instantiate(Widget parent) { 3877 auto n = factory(parent); 3878 n.name = name; 3879 foreach(child; children) 3880 child.instantiate(n); 3881 return n; 3882 } 3883 } 3884 3885 /++ 3886 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3887 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3888 3889 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3890 structures. It works fine on structs declared inside functions though. 3891 3892 See: https://issues.dlang.org/show_bug.cgi?id=21984 3893 +/ 3894 template Container(CArgs...) { 3895 static if(CArgs.length && is(CArgs[0] : Widget)) { 3896 private alias Super = CArgs[0]; 3897 private alias CArgs2 = CArgs[1 .. $]; 3898 } else { 3899 private alias Super = Layout; 3900 private alias CArgs2 = CArgs; 3901 } 3902 3903 class Container : Super { 3904 this(Widget parent) { super(parent); } 3905 3906 // just to partially support old gdc versions 3907 version(GNU) { 3908 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3909 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3910 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3911 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3912 } else mixin(q{ 3913 static foreach(Arg; CArgs2) { 3914 mixin Arg.MethodOverride!(Arg); 3915 } 3916 }); 3917 3918 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3919 return ContainerMeta( 3920 name, 3921 children.dup, 3922 function (Widget parent) { return new typeof(this)(parent); } 3923 ); 3924 } 3925 3926 static ContainerMeta opCall(ContainerMeta[] children...) { 3927 return opCall(null, children); 3928 } 3929 } 3930 } 3931 3932 /++ 3933 The data controller widget is created by reflecting over the given 3934 data type. You can use [ControlledBy] as a UDA on a struct or 3935 just let it create things automatically. 3936 3937 Unlike [dialog], this uses real-time updating of the data and 3938 you add it to another window yourself. 3939 3940 --- 3941 struct Test { 3942 int x; 3943 int y; 3944 } 3945 3946 auto window = new Window(); 3947 auto dcw = new DataControllerWidget!Test(new Test, window); 3948 --- 3949 3950 The way it works is any public members are given a widget based 3951 on their data type, and public methods trigger an action button 3952 if no relevant parameters or a dialog action if it does have 3953 parameters, similar to the [menu] facility. 3954 3955 If you change data programmatically, without going through the 3956 DataControllerWidget methods, you will have to tell it something 3957 has changed and it needs to redraw. This is done with the `invalidate` 3958 method. 3959 3960 History: 3961 Added Oct 28, 2020 3962 +/ 3963 /// Group: generating_from_code 3964 class DataControllerWidget(T) : WidgetContainer { 3965 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3966 private alias Tref = T; 3967 else 3968 private alias Tref = T*; 3969 3970 Tref datum; 3971 3972 /++ 3973 See_also: [addDataControllerWidget] 3974 +/ 3975 this(Tref datum, Widget parent) { 3976 this.datum = datum; 3977 3978 Widget cp = this; 3979 3980 super(parent); 3981 3982 foreach(attr; __traits(getAttributes, T)) 3983 static if(is(typeof(attr) == ContainerMeta)) { 3984 cp = attr.instantiate(this); 3985 } 3986 3987 auto def = this.getByName("default"); 3988 if(def !is null) 3989 cp = def; 3990 3991 Widget helper(string name) { 3992 auto maybe = this.getByName(name); 3993 if(maybe is null) 3994 return cp; 3995 return maybe; 3996 3997 } 3998 3999 foreach(member; __traits(allMembers, T)) 4000 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4001 static if(is(typeof(__traits(getMember, this.datum, member)))) 4002 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4003 void delegate() update; 4004 4005 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4006 4007 if(update) 4008 updaters ~= update; 4009 4010 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4011 w.addEventListener("triggered", delegate() { 4012 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 4013 notifyDataUpdated(); 4014 }); 4015 } else static if(is(typeof(w.isChecked) == bool)) { 4016 w.addEventListener(EventType.change, (Event ev) { 4017 __traits(getMember, this.datum, member) = w.isChecked; 4018 }); 4019 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4020 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4021 } else static if(is(typeof(w.value) == int)) { 4022 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4023 } else static if(is(typeof(w) == DropDownSelection)) { 4024 // special case for this to kinda support enums and such. coudl be better though 4025 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4026 } else { 4027 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4028 } 4029 } 4030 } 4031 4032 /++ 4033 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4034 4035 History: 4036 Added May 28, 2021 4037 +/ 4038 void notifyDataUpdated() { 4039 foreach(updater; updaters) 4040 updater(); 4041 4042 this.emit!(ChangeEvent!void)(delegate{}); 4043 } 4044 4045 private Widget[string] memberWidgets; 4046 private void delegate()[] updaters; 4047 4048 mixin Emits!(ChangeEvent!void); 4049 } 4050 4051 private int saturatedSum(int[] values...) { 4052 int sum; 4053 foreach(value; values) { 4054 if(value == int.max) 4055 return int.max; 4056 sum += value; 4057 } 4058 return sum; 4059 } 4060 4061 void genericSetValue(T, W)(T* where, W what) { 4062 import std.conv; 4063 *where = to!T(what); 4064 //*where = cast(T) stringToLong(what); 4065 } 4066 4067 /++ 4068 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4069 4070 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4071 4072 Note that this creates the widget but does not attach any event handlers to it. 4073 +/ 4074 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4075 4076 string displayName = __traits(identifier, tt).beautify; 4077 4078 static if(controlledByCount!tt == 1) { 4079 foreach(i, attr; __traits(getAttributes, tt)) { 4080 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4081 auto w = attr.construct(parent); 4082 static if(__traits(compiles, w.setPosition(*valptr))) 4083 update = () { w.setPosition(*valptr); }; 4084 else static if(__traits(compiles, w.setValue(*valptr))) 4085 update = () { w.setValue(*valptr); }; 4086 4087 if(update) 4088 update(); 4089 return w; 4090 } 4091 } 4092 } else static if(controlledByCount!tt == 0) { 4093 static if(is(typeof(tt) == enum)) { 4094 // FIXME: update 4095 auto dds = new DropDownSelection(parent); 4096 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4097 dds.addOption(option); 4098 if(__traits(getMember, typeof(tt), option) == *valptr) 4099 dds.setSelection(cast(int) idx); 4100 } 4101 return dds; 4102 } else static if(is(typeof(tt) == bool)) { 4103 auto box = new Checkbox(displayName, parent); 4104 update = () { box.isChecked = *valptr; }; 4105 update(); 4106 return box; 4107 } else static if(is(typeof(tt) : const long)) { 4108 auto le = new LabeledLineEdit(displayName, parent); 4109 update = () { le.content = toInternal!string(*valptr); }; 4110 update(); 4111 return le; 4112 } else static if(is(typeof(tt) : const double)) { 4113 auto le = new LabeledLineEdit(displayName, parent); 4114 import std.conv; 4115 update = () { le.content = to!string(*valptr); }; 4116 update(); 4117 return le; 4118 } else static if(is(typeof(tt) : const string)) { 4119 auto le = new LabeledLineEdit(displayName, parent); 4120 update = () { le.content = *valptr; }; 4121 update(); 4122 return le; 4123 } else static if(is(typeof(tt) == function)) { 4124 auto w = new Button(displayName, parent); 4125 return w; 4126 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4127 return parent.addDataControllerWidget(tt); 4128 } else static assert(0, typeof(tt).stringof); 4129 } else static assert(0, "multiple controllers not yet supported"); 4130 } 4131 4132 private template controlledByCount(alias tt) { 4133 static int helper() { 4134 int count; 4135 foreach(i, attr; __traits(getAttributes, tt)) 4136 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4137 count++; 4138 return count; 4139 } 4140 4141 enum controlledByCount = helper; 4142 } 4143 4144 /++ 4145 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4146 4147 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4148 4149 History: 4150 The `redrawOnChange` parameter was added on May 28, 2021. 4151 +/ 4152 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4153 auto dcw = new DataControllerWidget!T(t, parent); 4154 initializeDataControllerWidget(dcw, redrawOnChange); 4155 return dcw; 4156 } 4157 4158 /// ditto 4159 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4160 auto dcw = new DataControllerWidget!T(t, parent); 4161 initializeDataControllerWidget(dcw, redrawOnChange); 4162 return dcw; 4163 } 4164 4165 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4166 if(redrawOnChange !is null) 4167 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4168 } 4169 4170 /++ 4171 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. 4172 4173 History: 4174 Finalized on June 3, 2021 for the dub v10.0 release 4175 +/ 4176 struct StyleInformation { 4177 private Widget w; 4178 private BaseVisualTheme visualTheme; 4179 4180 private this(Widget w) { 4181 this.w = w; 4182 this.visualTheme = WidgetPainter.visualTheme; 4183 } 4184 4185 /++ 4186 Forwards to [Widget.Style] 4187 4188 Bugs: 4189 It is supposed to fall back to the [VisualTheme] if 4190 the style doesn't override the default, but that is 4191 not generally implemented. Many of them may end up 4192 being explicit overloads instead of the generic 4193 opDispatch fallback, like [font] is now. 4194 +/ 4195 public @property opDispatch(string name)() { 4196 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4197 w.useStyleProperties((scope Widget.Style props) { 4198 //visualTheme.useStyleProperties(w, (props) { 4199 prop = __traits(getMember, props, name); 4200 }); 4201 return prop; 4202 } 4203 4204 /++ 4205 Returns the cached font object associated with the widget, 4206 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4207 4208 History: 4209 Prior to March 21, 2022 (dub v10.7), `font` went through 4210 [opDispatch], which did not use the cache. You can now call it 4211 repeatedly without guilt. 4212 +/ 4213 public @property OperatingSystemFont font() { 4214 OperatingSystemFont prop; 4215 w.useStyleProperties((scope Widget.Style props) { 4216 prop = props.fontCached; 4217 }); 4218 if(prop is null) { 4219 prop = visualTheme.defaultFontCached(w.currentDpi); 4220 } 4221 return prop; 4222 } 4223 4224 @property { 4225 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4226 /** */ int paddingLeft() { return w.paddingLeft(); } 4227 /** */ int paddingRight() { return w.paddingRight(); } 4228 /** */ int paddingTop() { return w.paddingTop(); } 4229 /** */ int paddingBottom() { return w.paddingBottom(); } 4230 4231 /** */ int marginLeft() { return w.marginLeft(); } 4232 /** */ int marginRight() { return w.marginRight(); } 4233 /** */ int marginTop() { return w.marginTop(); } 4234 /** */ int marginBottom() { return w.marginBottom(); } 4235 4236 /** */ int maxHeight() { return w.maxHeight(); } 4237 /** */ int minHeight() { return w.minHeight(); } 4238 4239 /** */ int maxWidth() { return w.maxWidth(); } 4240 /** */ int minWidth() { return w.minWidth(); } 4241 4242 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4243 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4244 4245 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4246 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4247 4248 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4249 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4250 4251 // Global helpers some of these are unstable. 4252 static: 4253 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4254 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4255 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4256 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4257 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4258 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4259 4260 /** */ Color activeTabColor() { return lightAccentColor; } 4261 /** */ Color buttonColor() { return windowBackgroundColor; } 4262 /** */ Color depressedButtonColor() { return darkAccentColor; } 4263 /** */ Color hoveringColor() { return lightAccentColor; } 4264 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4265 auto c = WidgetPainter.visualTheme.selectionColor(); 4266 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4267 } 4268 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4269 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4270 } 4271 4272 4273 4274 /+ 4275 4276 private static auto extractStyleProperty(string name)(Widget w) { 4277 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4278 w.useStyleProperties((props) { 4279 prop = __traits(getMember, props, name); 4280 }); 4281 return prop; 4282 } 4283 4284 // FIXME: clear this upon a X server disconnect 4285 private static OperatingSystemFont[string] fontCache; 4286 4287 T getProperty(T)(string name, lazy T default_) { 4288 if(visualTheme !is null) { 4289 auto str = visualTheme.getPropertyString(w, name); 4290 if(str is null) 4291 return default_; 4292 static if(is(T == Color)) 4293 return Color.fromString(str); 4294 else static if(is(T == Measurement)) 4295 return Measurement(cast(int) toInternal!int(str)); 4296 else static if(is(T == WidgetBackground)) 4297 return WidgetBackground.fromString(str); 4298 else static if(is(T == OperatingSystemFont)) { 4299 if(auto f = str in fontCache) 4300 return *f; 4301 else 4302 return fontCache[str] = new OperatingSystemFont(str); 4303 } else static if(is(T == FrameStyle)) { 4304 switch(str) { 4305 default: 4306 return FrameStyle.none; 4307 foreach(style; __traits(allMembers, FrameStyle)) 4308 case style: 4309 return __traits(getMember, FrameStyle, style); 4310 } 4311 } else static assert(0); 4312 } else 4313 return default_; 4314 } 4315 4316 static struct Measurement { 4317 int value; 4318 alias value this; 4319 } 4320 4321 @property: 4322 4323 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4324 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4325 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4326 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4327 4328 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4329 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4330 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4331 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4332 4333 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4334 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4335 4336 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4337 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4338 4339 4340 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4341 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4342 4343 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4344 4345 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4346 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4347 4348 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4349 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4350 4351 4352 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4353 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4354 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4355 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4356 4357 Color activeTabColor() { return lightAccentColor; } 4358 Color buttonColor() { return windowBackgroundColor; } 4359 Color depressedButtonColor() { return darkAccentColor; } 4360 Color hoveringColor() { return Color(228, 228, 228); } 4361 Color activeListXorColor() { 4362 auto c = WidgetPainter.visualTheme.selectionColor(); 4363 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4364 } 4365 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4366 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4367 +/ 4368 } 4369 4370 4371 4372 // pragma(msg, __traits(classInstanceSize, Widget)); 4373 4374 /*private*/ template EventString(E) { 4375 static if(is(typeof(E.EventString))) 4376 enum EventString = E.EventString; 4377 else 4378 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4379 } 4380 4381 /*private*/ template EventStringIdentifier(E) { 4382 string helper() { 4383 auto es = EventString!E; 4384 char[] id = new char[](es.length * 2); 4385 size_t idx; 4386 foreach(char ch; es) { 4387 id[idx++] = cast(char)('a' + (ch >> 4)); 4388 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4389 } 4390 return cast(string) id; 4391 } 4392 4393 enum EventStringIdentifier = helper(); 4394 } 4395 4396 4397 template classStaticallyEmits(This, EventType) { 4398 static if(is(This Base == super)) 4399 static if(is(Base : Widget)) 4400 enum baseEmits = classStaticallyEmits!(Base, EventType); 4401 else 4402 enum baseEmits = false; 4403 else 4404 enum baseEmits = false; 4405 4406 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4407 4408 enum classStaticallyEmits = thisEmits || baseEmits; 4409 } 4410 4411 /++ 4412 A helper to make widgets out of other native windows. 4413 4414 History: 4415 Factored out of OpenGlWidget on November 5, 2021 4416 +/ 4417 class NestedChildWindowWidget : Widget { 4418 SimpleWindow win; 4419 4420 /++ 4421 Used on X to send focus to the appropriate child window when requested by the window manager. 4422 4423 Normally returns its own nested window. Can also return another child or null to revert to the parent 4424 if you override it in a child class. 4425 4426 History: 4427 Added April 2, 2022 (dub v10.8) 4428 +/ 4429 SimpleWindow focusableWindow() { 4430 return win; 4431 } 4432 4433 /// 4434 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4435 this(SimpleWindow win, Widget parent) { 4436 this.parentWindow = parent.parentWindow; 4437 this.win = win; 4438 4439 super(parent); 4440 windowsetup(win); 4441 } 4442 4443 static protected SimpleWindow getParentWindow(Widget parent) { 4444 assert(parent !is null); 4445 SimpleWindow pwin = parent.parentWindow.win; 4446 4447 version(win32_widgets) { 4448 HWND phwnd; 4449 auto wtf = parent; 4450 while(wtf) { 4451 if(wtf.hwnd) { 4452 phwnd = wtf.hwnd; 4453 break; 4454 } 4455 wtf = wtf.parent; 4456 } 4457 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4458 if(phwnd) 4459 pwin = new SimpleWindow(phwnd); 4460 } 4461 4462 return pwin; 4463 } 4464 4465 /++ 4466 Called upon the nested window being destroyed. 4467 Remember the window has already been destroyed at 4468 this point, so don't use the native handle for anything. 4469 4470 History: 4471 Added April 3, 2022 (dub v10.8) 4472 +/ 4473 protected void dispose() { 4474 4475 } 4476 4477 protected void windowsetup(SimpleWindow w) { 4478 /* 4479 win.onFocusChange = (bool getting) { 4480 if(getting) 4481 this.focus(); 4482 }; 4483 */ 4484 4485 /+ 4486 win.onFocusChange = (bool getting) { 4487 if(getting) { 4488 this.parentWindow.focusedWidget = this; 4489 this.emit!FocusEvent(); 4490 this.emit!FocusInEvent(); 4491 } else { 4492 this.emit!BlurEvent(); 4493 this.emit!FocusOutEvent(); 4494 } 4495 }; 4496 +/ 4497 4498 win.onDestroyed = () { 4499 this.dispose(); 4500 }; 4501 4502 version(win32_widgets) { 4503 Widget.nativeMapping[win.hwnd] = this; 4504 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4505 } else { 4506 win.setEventHandlers( 4507 (MouseEvent e) { 4508 Widget p = this; 4509 while(p ! is parentWindow) { 4510 e.x += p.x; 4511 e.y += p.y; 4512 p = p.parent; 4513 } 4514 parentWindow.dispatchMouseEvent(e); 4515 }, 4516 (KeyEvent e) { 4517 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4518 parentWindow.dispatchKeyEvent(e); 4519 }, 4520 (dchar e) { 4521 parentWindow.dispatchCharEvent(e); 4522 }, 4523 ); 4524 } 4525 4526 } 4527 4528 override void showing(bool s, bool recalc) { 4529 auto cur = hidden; 4530 win.hidden = !s; 4531 if(cur != s && s) 4532 redraw(); 4533 } 4534 4535 /// OpenGL widgets cannot have child widgets. Do not call this. 4536 /* @disable */ final override void addChild(Widget, int) { 4537 throw new Error("cannot add children to OpenGL widgets"); 4538 } 4539 4540 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4541 /// Keep in mind that events like mouse coordinates are still relative to your size. 4542 override void registerMovement() { 4543 // writefln("%d %d %d %d", x,y,width,height); 4544 version(win32_widgets) 4545 auto pos = getChildPositionRelativeToParentHwnd(this); 4546 else 4547 auto pos = getChildPositionRelativeToParentOrigin(this); 4548 win.moveResize(pos[0], pos[1], width, height); 4549 4550 registerMovementAdditionalWork(); 4551 sendResizeEvent(); 4552 } 4553 4554 abstract void registerMovementAdditionalWork(); 4555 } 4556 4557 /++ 4558 Nests an opengl capable window inside this window as a widget. 4559 4560 You may also just want to create an additional [SimpleWindow] with 4561 [OpenGlOptions.yes] yourself. 4562 4563 An OpenGL widget cannot have child widgets. It will throw if you try. 4564 +/ 4565 static if(OpenGlEnabled) 4566 class OpenGlWidget : NestedChildWindowWidget { 4567 4568 override void registerMovementAdditionalWork() { 4569 win.setAsCurrentOpenGlContext(); 4570 } 4571 4572 /// 4573 this(Widget parent) { 4574 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4575 super(win, parent); 4576 } 4577 4578 override void paint(WidgetPainter painter) { 4579 win.setAsCurrentOpenGlContext(); 4580 glViewport(0, 0, this.width, this.height); 4581 win.redrawOpenGlSceneNow(); 4582 } 4583 4584 void redrawOpenGlScene(void delegate() dg) { 4585 win.redrawOpenGlScene = dg; 4586 } 4587 } 4588 4589 /++ 4590 This demo shows how to draw text in an opengl scene. 4591 +/ 4592 unittest { 4593 import arsd.minigui; 4594 import arsd.ttf; 4595 4596 void main() { 4597 auto window = new Window(); 4598 4599 auto widget = new OpenGlWidget(window); 4600 4601 // old means non-shader code so compatible with glBegin etc. 4602 // tbh I haven't implemented new one in font yet... 4603 // anyway, declaring here, will construct soon. 4604 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4605 4606 // this is a little bit awkward, calling some methods through 4607 // the underlying SimpleWindow `win` method, and you can't do this 4608 // on a nanovega widget due to conflicts so I should probably fix 4609 // the api to be a bit easier. But here it will work. 4610 // 4611 // Alternatively, you could load the font on the first draw, inside 4612 // the redrawOpenGlScene, and keep a flag so you don't do it every 4613 // time. That'd be a bit easier since the lib sets up the context 4614 // by then guaranteed. 4615 // 4616 // But still, I wanna show this. 4617 widget.win.visibleForTheFirstTime = delegate { 4618 // must set the opengl context 4619 widget.win.setAsCurrentOpenGlContext(); 4620 4621 // if you were doing a OpenGL 3+ shader, this 4622 // gets especially important to do in order. With 4623 // old-style opengl, I think you can even do it 4624 // in main(), but meh, let's show it more correctly. 4625 4626 // Anyway, now it is time to load the font from the 4627 // OS (you can alternatively load one from a .ttf file 4628 // you bundle with the application), then load the 4629 // font into texture for drawing. 4630 4631 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4632 4633 assert(!osfont.isNull()); // make sure it actually loaded 4634 4635 // using typeof to avoid repeating the long name lol 4636 glfont = new typeof(glfont)( 4637 // get the raw data from the font for loading in here 4638 // since it doesn't use the OS function to draw the 4639 // text, we gotta treat it more as a file than as 4640 // a drawing api. 4641 osfont.getTtfBytes(), 4642 18, // need to respecify size since opengl world is different coordinate system 4643 4644 // these last two numbers are why it is called 4645 // "Limited" font. It only loads the characters 4646 // in the given range, since the texture atlas 4647 // it references is all a big image generated ahead 4648 // of time. You could maybe do the whole thing but 4649 // idk how much memory that is. 4650 // 4651 // But here, 0-128 represents the ASCII range, so 4652 // good enough for most English things, numeric labels, 4653 // etc. 4654 0, 4655 128 4656 ); 4657 }; 4658 4659 widget.redrawOpenGlScene = () { 4660 // now we can use the glfont's drawString function 4661 4662 // first some opengl setup. You can do this in one place 4663 // on window first visible too in many cases, just showing 4664 // here cuz it is easier for me. 4665 4666 // gonna need some alpha blending or it just looks awful 4667 glEnable(GL_BLEND); 4668 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4669 glClearColor(0,0,0,0); 4670 glDepthFunc(GL_LEQUAL); 4671 4672 // Also need to enable 2d textures, since it draws the 4673 // font characters as images baked in 4674 glMatrixMode(GL_MODELVIEW); 4675 glLoadIdentity(); 4676 glDisable(GL_DEPTH_TEST); 4677 glEnable(GL_TEXTURE_2D); 4678 4679 // the orthographic matrix is best for 2d things like text 4680 // so let's set that up. This matrix makes the coordinates 4681 // in the opengl scene be one-to-one with the actual pixels 4682 // on screen. (Not necessarily best, you may wish to scale 4683 // things, but it does help keep fonts looking normal.) 4684 glMatrixMode(GL_PROJECTION); 4685 glLoadIdentity(); 4686 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4687 4688 // you can do other glScale, glRotate, glTranslate, etc 4689 // to the matrix here of course if you want. 4690 4691 // note the x,y coordinates here are for the text baseline 4692 // NOT the upper-left corner. The baseline is like the line 4693 // in the notebook you write on. Most the letters are actually 4694 // above it, but some, like p and q, dip a bit below it. 4695 // 4696 // So if you're used to the upper left coordinate like the 4697 // rest of simpledisplay/minigui usually do, do the 4698 // y + glfont.ascent to bring it down a little. So this 4699 // example puts the string in the upper left of the window. 4700 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4701 4702 // re color btw: the function sets a solid color internally, 4703 // but you actually COULD do your own thing for rainbow effects 4704 // and the sort if you wanted too, by pulling its guts out. 4705 // Just view its source for an idea of how it actually draws: 4706 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4707 4708 // it gets a bit complicated with the character positioning, 4709 // but the opengl parts are fairly simple: bind a texture, 4710 // set the color, draw a quad for each letter. 4711 4712 4713 // the last optional argument there btw is a bounding box 4714 // it will/ use to word wrap and return an object you can 4715 // use to implement scrolling or pagination; it tells how 4716 // much of the string didn't fit in the box. But for simple 4717 // labels we can just ignore that. 4718 4719 4720 // I'd suggest drawing text as the last step, after you 4721 // do your other drawing. You might use the push/pop matrix 4722 // stuff to keep your place. You, in theory, should be able 4723 // to do text in a 3d space but I've never actually tried 4724 // that.... 4725 }; 4726 4727 window.loop(); 4728 } 4729 } 4730 4731 version(custom_widgets) 4732 private alias ListWidgetBase = ScrollableWidget; 4733 else 4734 private alias ListWidgetBase = Widget; 4735 4736 /++ 4737 A list widget contains a list of strings that the user can examine and select. 4738 4739 4740 In the future, items in the list may be possible to be more than just strings. 4741 4742 See_Also: 4743 [TableView] 4744 +/ 4745 class ListWidget : ListWidgetBase { 4746 /// 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. 4747 mixin Emits!(ChangeEvent!void); 4748 4749 static struct Option { 4750 string label; 4751 bool selected; 4752 void* tag; 4753 } 4754 4755 /++ 4756 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4757 +/ 4758 void setSelection(int y) { 4759 if(!multiSelect) 4760 foreach(ref opt; options) 4761 opt.selected = false; 4762 if(y >= 0 && y < options.length) 4763 options[y].selected = !options[y].selected; 4764 4765 this.emit!(ChangeEvent!void)(delegate {}); 4766 4767 version(custom_widgets) 4768 redraw(); 4769 } 4770 4771 /++ 4772 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4773 Returns -1 if nothing is selected. 4774 +/ 4775 int getSelection() 4776 { 4777 foreach(i, opt; options) { 4778 if (opt.selected) 4779 return cast(int) i; 4780 } 4781 return -1; 4782 } 4783 4784 version(custom_widgets) 4785 override void defaultEventHandler_click(ClickEvent event) { 4786 this.focus(); 4787 if(event.button == MouseButton.left) { 4788 auto y = (event.clientY - 4) / defaultLineHeight; 4789 if(y >= 0 && y < options.length) { 4790 setSelection(y); 4791 } 4792 } 4793 super.defaultEventHandler_click(event); 4794 } 4795 4796 this(Widget parent) { 4797 tabStop = false; 4798 super(parent); 4799 version(win32_widgets) 4800 createWin32Window(this, WC_LISTBOX, "", 4801 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4802 } 4803 4804 version(win32_widgets) 4805 override void handleWmCommand(ushort code, ushort id) { 4806 switch(code) { 4807 case LBN_SELCHANGE: 4808 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4809 setSelection(cast(int) sel); 4810 break; 4811 default: 4812 } 4813 } 4814 4815 4816 version(custom_widgets) 4817 override void paintFrameAndBackground(WidgetPainter painter) { 4818 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4819 } 4820 4821 version(custom_widgets) 4822 override void paint(WidgetPainter painter) { 4823 auto cs = getComputedStyle(); 4824 auto pos = Point(4, 4); 4825 foreach(idx, option; options) { 4826 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4827 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4828 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4829 if(option.selected) { 4830 //painter.rasterOp = RasterOp.xor; 4831 painter.outlineColor = cs.selectionForegroundColor; 4832 painter.fillColor = cs.selectionBackgroundColor; 4833 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4834 //painter.rasterOp = RasterOp.normal; 4835 } 4836 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4837 painter.drawText(pos, option.label); 4838 pos.y += defaultLineHeight; 4839 } 4840 } 4841 4842 static class Style : Widget.Style { 4843 override WidgetBackground background() { 4844 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4845 } 4846 } 4847 mixin OverrideStyle!Style; 4848 //mixin Padding!q{2}; 4849 4850 void addOption(string text, void* tag = null) { 4851 options ~= Option(text, false, tag); 4852 version(win32_widgets) { 4853 WCharzBuffer buffer = WCharzBuffer(text); 4854 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4855 } 4856 version(custom_widgets) { 4857 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4858 redraw(); 4859 } 4860 } 4861 4862 void clear() { 4863 options = null; 4864 version(win32_widgets) { 4865 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4866 {} 4867 4868 } else version(custom_widgets) { 4869 scrollTo(Point(0, 0)); 4870 redraw(); 4871 } 4872 } 4873 4874 Option[] options; 4875 version(win32_widgets) 4876 enum multiSelect = false; /// not implemented yet 4877 else 4878 bool multiSelect; 4879 4880 override int heightStretchiness() { return 6; } 4881 } 4882 4883 4884 4885 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4886 enum ScrollBarShowPolicy { 4887 automatic, /// automatically show the scroll bar if it is necessary 4888 never, /// never show the scroll bar (scrolling must be done programmatically) 4889 always /// always show the scroll bar, even if it is disabled 4890 } 4891 4892 /++ 4893 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4894 4895 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4896 +/ 4897 // FIXME ScrollBarShowPolicy 4898 // FIXME: use the ScrollMessageWidget in here now that it exists 4899 class ScrollableWidget : Widget { 4900 // FIXME: make line size configurable 4901 // FIXME: add keyboard controls 4902 version(win32_widgets) { 4903 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4904 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4905 auto pos = HIWORD(wParam); 4906 auto m = LOWORD(wParam); 4907 4908 // FIXME: I can reintroduce the 4909 // scroll bars now by using this 4910 // in the top-level window handler 4911 // to forward comamnds 4912 auto scrollbarHwnd = lParam; 4913 switch(m) { 4914 case SB_BOTTOM: 4915 if(msg == WM_HSCROLL) 4916 horizontalScrollTo(contentWidth_); 4917 else 4918 verticalScrollTo(contentHeight_); 4919 break; 4920 case SB_TOP: 4921 if(msg == WM_HSCROLL) 4922 horizontalScrollTo(0); 4923 else 4924 verticalScrollTo(0); 4925 break; 4926 case SB_ENDSCROLL: 4927 // idk 4928 break; 4929 case SB_LINEDOWN: 4930 if(msg == WM_HSCROLL) 4931 horizontalScroll(scaleWithDpi(16)); 4932 else 4933 verticalScroll(scaleWithDpi(16)); 4934 break; 4935 case SB_LINEUP: 4936 if(msg == WM_HSCROLL) 4937 horizontalScroll(scaleWithDpi(-16)); 4938 else 4939 verticalScroll(scaleWithDpi(-16)); 4940 break; 4941 case SB_PAGEDOWN: 4942 if(msg == WM_HSCROLL) 4943 horizontalScroll(scaleWithDpi(100)); 4944 else 4945 verticalScroll(scaleWithDpi(100)); 4946 break; 4947 case SB_PAGEUP: 4948 if(msg == WM_HSCROLL) 4949 horizontalScroll(scaleWithDpi(-100)); 4950 else 4951 verticalScroll(scaleWithDpi(-100)); 4952 break; 4953 case SB_THUMBPOSITION: 4954 case SB_THUMBTRACK: 4955 if(msg == WM_HSCROLL) 4956 horizontalScrollTo(pos); 4957 else 4958 verticalScrollTo(pos); 4959 4960 if(m == SB_THUMBTRACK) { 4961 // the event loop doesn't seem to carry on with a requested redraw.. 4962 // so we request it to get our dirty bit set... 4963 redraw(); 4964 4965 // then we need to immediately actually redraw it too for instant feedback to user 4966 4967 SimpleWindow.processAllCustomEvents(); 4968 //if(parentWindow) 4969 //parentWindow.actualRedraw(); 4970 } 4971 break; 4972 default: 4973 } 4974 } 4975 return super.hookedWndProc(msg, wParam, lParam); 4976 } 4977 } 4978 /// 4979 this(Widget parent) { 4980 this.parentWindow = parent.parentWindow; 4981 4982 version(win32_widgets) { 4983 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4984 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4985 super(parent); 4986 } else version(custom_widgets) { 4987 outerContainer = new InternalScrollableContainerWidget(this, parent); 4988 super(outerContainer); 4989 } else static assert(0); 4990 } 4991 4992 version(custom_widgets) 4993 InternalScrollableContainerWidget outerContainer; 4994 4995 override void defaultEventHandler_click(ClickEvent event) { 4996 if(event.button == MouseButton.wheelUp) 4997 verticalScroll(scaleWithDpi(-16)); 4998 if(event.button == MouseButton.wheelDown) 4999 verticalScroll(scaleWithDpi(16)); 5000 super.defaultEventHandler_click(event); 5001 } 5002 5003 override void defaultEventHandler_keydown(KeyDownEvent event) { 5004 switch(event.key) { 5005 case Key.Left: 5006 horizontalScroll(scaleWithDpi(-16)); 5007 break; 5008 case Key.Right: 5009 horizontalScroll(scaleWithDpi(16)); 5010 break; 5011 case Key.Up: 5012 verticalScroll(scaleWithDpi(-16)); 5013 break; 5014 case Key.Down: 5015 verticalScroll(scaleWithDpi(16)); 5016 break; 5017 case Key.Home: 5018 verticalScrollTo(0); 5019 break; 5020 case Key.End: 5021 verticalScrollTo(contentHeight); 5022 break; 5023 case Key.PageUp: 5024 verticalScroll(scaleWithDpi(-160)); 5025 break; 5026 case Key.PageDown: 5027 verticalScroll(scaleWithDpi(160)); 5028 break; 5029 default: 5030 } 5031 super.defaultEventHandler_keydown(event); 5032 } 5033 5034 5035 version(win32_widgets) 5036 override void recomputeChildLayout() { 5037 super.recomputeChildLayout(); 5038 SCROLLINFO info; 5039 info.cbSize = info.sizeof; 5040 info.nPage = viewportHeight; 5041 info.fMask = SIF_PAGE | SIF_RANGE; 5042 info.nMin = 0; 5043 info.nMax = contentHeight_; 5044 SetScrollInfo(hwnd, SB_VERT, &info, true); 5045 5046 info.cbSize = info.sizeof; 5047 info.nPage = viewportWidth; 5048 info.fMask = SIF_PAGE | SIF_RANGE; 5049 info.nMin = 0; 5050 info.nMax = contentWidth_; 5051 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5052 } 5053 5054 /* 5055 Scrolling 5056 ------------ 5057 5058 You are assigned a width and a height by the layout engine, which 5059 is your viewport box. However, you may draw more than that by setting 5060 a contentWidth and contentHeight. 5061 5062 If these can be contained by the viewport, no scrollbar is displayed. 5063 If they cannot fit though, it will automatically show scroll as necessary. 5064 5065 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5066 is zero, no vertical scrolling is performed. 5067 5068 If scrolling is necessary, the lib will automatically work with the bars. 5069 When you redraw, the origin and clipping info in the painter is set so if 5070 you just draw everything, it will work, but you can be more efficient by checking 5071 the viewportWidth, viewportHeight, and scrollOrigin members. 5072 */ 5073 5074 /// 5075 final @property int viewportWidth() { 5076 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5077 } 5078 /// 5079 final @property int viewportHeight() { 5080 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5081 } 5082 5083 // FIXME property 5084 Point scrollOrigin_; 5085 5086 /// 5087 final const(Point) scrollOrigin() { 5088 return scrollOrigin_; 5089 } 5090 5091 // the user sets these two 5092 private int contentWidth_ = 0; 5093 private int contentHeight_ = 0; 5094 5095 /// 5096 int contentWidth() { return contentWidth_; } 5097 /// 5098 int contentHeight() { return contentHeight_; } 5099 5100 /// 5101 void setContentSize(int width, int height) { 5102 contentWidth_ = width; 5103 contentHeight_ = height; 5104 5105 version(custom_widgets) { 5106 if(showingVerticalScroll || showingHorizontalScroll) { 5107 outerContainer.recomputeChildLayout(); 5108 } 5109 5110 if(showingVerticalScroll()) 5111 outerContainer.verticalScrollBar.redraw(); 5112 if(showingHorizontalScroll()) 5113 outerContainer.horizontalScrollBar.redraw(); 5114 } else version(win32_widgets) { 5115 recomputeChildLayout(); 5116 } else static assert(0); 5117 } 5118 5119 /// 5120 void verticalScroll(int delta) { 5121 verticalScrollTo(scrollOrigin.y + delta); 5122 } 5123 /// 5124 void verticalScrollTo(int pos) { 5125 scrollOrigin_.y = pos; 5126 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5127 scrollOrigin_.y = contentHeight - viewportHeight; 5128 5129 if(scrollOrigin_.y < 0) 5130 scrollOrigin_.y = 0; 5131 5132 version(win32_widgets) { 5133 SCROLLINFO info; 5134 info.cbSize = info.sizeof; 5135 info.fMask = SIF_POS; 5136 info.nPos = scrollOrigin_.y; 5137 SetScrollInfo(hwnd, SB_VERT, &info, true); 5138 } else version(custom_widgets) { 5139 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5140 } else static assert(0); 5141 5142 redraw(); 5143 } 5144 5145 /// 5146 void horizontalScroll(int delta) { 5147 horizontalScrollTo(scrollOrigin.x + delta); 5148 } 5149 /// 5150 void horizontalScrollTo(int pos) { 5151 scrollOrigin_.x = pos; 5152 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5153 scrollOrigin_.x = contentWidth - viewportWidth; 5154 5155 if(scrollOrigin_.x < 0) 5156 scrollOrigin_.x = 0; 5157 5158 version(win32_widgets) { 5159 SCROLLINFO info; 5160 info.cbSize = info.sizeof; 5161 info.fMask = SIF_POS; 5162 info.nPos = scrollOrigin_.x; 5163 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5164 } else version(custom_widgets) { 5165 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5166 } else static assert(0); 5167 5168 redraw(); 5169 } 5170 /// 5171 void scrollTo(Point p) { 5172 verticalScrollTo(p.y); 5173 horizontalScrollTo(p.x); 5174 } 5175 5176 /// 5177 void ensureVisibleInScroll(Point p) { 5178 auto rect = viewportRectangle(); 5179 if(rect.contains(p)) 5180 return; 5181 if(p.x < rect.left) 5182 horizontalScroll(p.x - rect.left); 5183 else if(p.x > rect.right) 5184 horizontalScroll(p.x - rect.right); 5185 5186 if(p.y < rect.top) 5187 verticalScroll(p.y - rect.top); 5188 else if(p.y > rect.bottom) 5189 verticalScroll(p.y - rect.bottom); 5190 } 5191 5192 /// 5193 void ensureVisibleInScroll(Rectangle rect) { 5194 ensureVisibleInScroll(rect.upperLeft); 5195 ensureVisibleInScroll(rect.lowerRight); 5196 } 5197 5198 /// 5199 Rectangle viewportRectangle() { 5200 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5201 } 5202 5203 /// 5204 bool showingHorizontalScroll() { 5205 return contentWidth > width; 5206 } 5207 /// 5208 bool showingVerticalScroll() { 5209 return contentHeight > height; 5210 } 5211 5212 /// This is called before the ordinary paint delegate, 5213 /// giving you a chance to draw the window frame, etc, 5214 /// before the scroll clip takes effect 5215 void paintFrameAndBackground(WidgetPainter painter) { 5216 version(win32_widgets) { 5217 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5218 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5219 // since the pen is null, to fill the whole space, we need the +1 on both. 5220 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5221 SelectObject(painter.impl.hdc, p); 5222 SelectObject(painter.impl.hdc, b); 5223 } 5224 5225 } 5226 5227 // make space for the scroll bar, and that's it. 5228 final override int paddingRight() { return scaleWithDpi(16); } 5229 final override int paddingBottom() { return scaleWithDpi(16); } 5230 5231 /* 5232 END SCROLLING 5233 */ 5234 5235 override WidgetPainter draw() { 5236 int x = this.x, y = this.y; 5237 auto parent = this.parent; 5238 while(parent) { 5239 x += parent.x; 5240 y += parent.y; 5241 parent = parent.parent; 5242 } 5243 5244 //version(win32_widgets) { 5245 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5246 //} else { 5247 auto painter = parentWindow.win.draw(true); 5248 //} 5249 painter.originX = x; 5250 painter.originY = y; 5251 5252 painter.originX = painter.originX - scrollOrigin.x; 5253 painter.originY = painter.originY - scrollOrigin.y; 5254 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5255 5256 return WidgetPainter(painter, this); 5257 } 5258 5259 mixin ScrollableChildren; 5260 } 5261 5262 // you need to have a Point scrollOrigin in the class somewhere 5263 // and a paintFrameAndBackground 5264 private mixin template ScrollableChildren() { 5265 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5266 if(hidden) 5267 return; 5268 5269 //version(win32_widgets) 5270 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5271 5272 painter.originX = lox + x; 5273 painter.originY = loy + y; 5274 5275 bool actuallyPainted = false; 5276 5277 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5278 if(clip == Rectangle.init) 5279 return; 5280 5281 if(force || redrawRequested) { 5282 //painter.setClipRectangle(scrollOrigin, width, height); 5283 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5284 paintFrameAndBackground(painter); 5285 } 5286 5287 painter.originX = painter.originX - scrollOrigin.x; 5288 painter.originY = painter.originY - scrollOrigin.y; 5289 if(force || redrawRequested) { 5290 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5291 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5292 5293 //erase(painter); // we paintFrameAndBackground above so no need 5294 if(painter.visualTheme) 5295 painter.visualTheme.doPaint(this, painter); 5296 else 5297 paint(painter); 5298 5299 if(invalidate) { 5300 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5301 // children are contained inside this, so no need to do extra work 5302 invalidate = false; 5303 } 5304 5305 5306 actuallyPainted = true; 5307 redrawRequested = false; 5308 } 5309 foreach(child; children) { 5310 if(cast(FixedPosition) child) 5311 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5312 else 5313 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5314 } 5315 } 5316 } 5317 5318 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5319 ScrollableContainerWidget scw; 5320 5321 this(ScrollableContainerWidget parent) { 5322 scw = parent; 5323 super(parent); 5324 } 5325 5326 version(custom_widgets) 5327 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5328 if(hidden) 5329 return; 5330 5331 bool actuallyPainted = false; 5332 5333 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5334 5335 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5336 if(clip == Rectangle.init) 5337 return; 5338 5339 painter.originX = lox + x - scrollOrigin.x; 5340 painter.originY = loy + y - scrollOrigin.y; 5341 if(force || redrawRequested) { 5342 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5343 5344 erase(painter); 5345 if(painter.visualTheme) 5346 painter.visualTheme.doPaint(this, painter); 5347 else 5348 paint(painter); 5349 5350 if(invalidate) { 5351 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5352 // children are contained inside this, so no need to do extra work 5353 invalidate = false; 5354 } 5355 5356 actuallyPainted = true; 5357 redrawRequested = false; 5358 } 5359 foreach(child; children) { 5360 if(cast(FixedPosition) child) 5361 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5362 else 5363 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5364 } 5365 } 5366 5367 version(custom_widgets) 5368 override protected void addScrollPosition(ref int x, ref int y) { 5369 x += scw.scrollX_; 5370 y += scw.scrollY_; 5371 } 5372 } 5373 5374 /++ 5375 A widget meant to contain other widgets that may need to scroll. 5376 5377 Currently buggy. 5378 5379 History: 5380 Added July 1, 2021 (dub v10.2) 5381 5382 On January 3, 2022, I tried to use it in a few other cases 5383 and found it only worked well in the original test case. Since 5384 it still sucks, I think I'm going to rewrite it again. 5385 +/ 5386 class ScrollableContainerWidget : ContainerWidget { 5387 /// 5388 this(Widget parent) { 5389 super(parent); 5390 5391 container = new InternalScrollableContainerInsideWidget(this); 5392 hsb = new HorizontalScrollbar(this); 5393 vsb = new VerticalScrollbar(this); 5394 5395 tabStop = false; 5396 container.tabStop = false; 5397 magic = true; 5398 5399 5400 vsb.addEventListener("scrolltonextline", () { 5401 scrollBy(0, scaleWithDpi(16)); 5402 }); 5403 vsb.addEventListener("scrolltopreviousline", () { 5404 scrollBy(0,scaleWithDpi( -16)); 5405 }); 5406 vsb.addEventListener("scrolltonextpage", () { 5407 scrollBy(0, container.height); 5408 }); 5409 vsb.addEventListener("scrolltopreviouspage", () { 5410 scrollBy(0, -container.height); 5411 }); 5412 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5413 scrollTo(scrollX_, spe.value); 5414 }); 5415 5416 this.addEventListener(delegate (scope ClickEvent e) { 5417 if(e.button == MouseButton.wheelUp) { 5418 if(!e.defaultPrevented) 5419 scrollBy(0, scaleWithDpi(-16)); 5420 e.stopPropagation(); 5421 } else if(e.button == MouseButton.wheelDown) { 5422 if(!e.defaultPrevented) 5423 scrollBy(0, scaleWithDpi(16)); 5424 e.stopPropagation(); 5425 } 5426 }); 5427 } 5428 5429 /+ 5430 override void defaultEventHandler_click(ClickEvent e) { 5431 } 5432 +/ 5433 5434 override void removeAllChildren() { 5435 container.removeAllChildren(); 5436 } 5437 5438 void scrollTo(int x, int y) { 5439 scrollBy(x - scrollX_, y - scrollY_); 5440 } 5441 5442 void scrollBy(int x, int y) { 5443 auto ox = scrollX_; 5444 auto oy = scrollY_; 5445 5446 auto nx = ox + x; 5447 auto ny = oy + y; 5448 5449 if(nx < 0) 5450 nx = 0; 5451 if(ny < 0) 5452 ny = 0; 5453 5454 auto maxX = hsb.max - container.width; 5455 if(maxX < 0) maxX = 0; 5456 auto maxY = vsb.max - container.height; 5457 if(maxY < 0) maxY = 0; 5458 5459 if(nx > maxX) 5460 nx = maxX; 5461 if(ny > maxY) 5462 ny = maxY; 5463 5464 auto dx = nx - ox; 5465 auto dy = ny - oy; 5466 5467 if(dx || dy) { 5468 version(win32_widgets) 5469 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5470 else { 5471 redraw(); 5472 } 5473 5474 hsb.setPosition = nx; 5475 vsb.setPosition = ny; 5476 5477 scrollX_ = nx; 5478 scrollY_ = ny; 5479 } 5480 } 5481 5482 private int scrollX_; 5483 private int scrollY_; 5484 5485 void setTotalArea(int width, int height) { 5486 hsb.setMax(width); 5487 vsb.setMax(height); 5488 } 5489 5490 /// 5491 void setViewableArea(int width, int height) { 5492 hsb.setViewableArea(width); 5493 vsb.setViewableArea(height); 5494 } 5495 5496 private bool magic; 5497 override void addChild(Widget w, int position = int.max) { 5498 if(magic) 5499 container.addChild(w, position); 5500 else 5501 super.addChild(w, position); 5502 } 5503 5504 override void recomputeChildLayout() { 5505 if(hsb is null || vsb is null || container is null) return; 5506 5507 /+ 5508 writeln(x, " ", y , " ", width, " ", height); 5509 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5510 +/ 5511 5512 registerMovement(); 5513 5514 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5515 hsb.x = 0; 5516 hsb.y = this.height - hsb.height; 5517 hsb.width = this.width - scaleWithDpi(16); 5518 hsb.recomputeChildLayout(); 5519 5520 vsb.width = scaleWithDpi(16); // FIXME? 5521 vsb.x = this.width - vsb.width; 5522 vsb.y = 0; 5523 vsb.height = this.height - scaleWithDpi(16); 5524 vsb.recomputeChildLayout(); 5525 5526 container.x = 0; 5527 container.y = 0; 5528 container.width = this.width - vsb.width; 5529 container.height = this.height - hsb.height; 5530 container.recomputeChildLayout(); 5531 5532 scrollX_ = 0; 5533 scrollY_ = 0; 5534 5535 hsb.setPosition(0); 5536 vsb.setPosition(0); 5537 5538 int mw, mh; 5539 Widget c = container; 5540 // FIXME: hack here to handle a layout inside... 5541 if(c.children.length == 1 && cast(Layout) c.children[0]) 5542 c = c.children[0]; 5543 foreach(child; c.children) { 5544 auto w = child.x + child.width; 5545 auto h = child.y + child.height; 5546 5547 if(w > mw) mw = w; 5548 if(h > mh) mh = h; 5549 } 5550 5551 setTotalArea(mw, mh); 5552 setViewableArea(width, height); 5553 } 5554 5555 override int minHeight() { return scaleWithDpi(64); } 5556 5557 HorizontalScrollbar hsb; 5558 VerticalScrollbar vsb; 5559 ContainerWidget container; 5560 } 5561 5562 5563 version(custom_widgets) 5564 private class InternalScrollableContainerWidget : Widget { 5565 5566 ScrollableWidget sw; 5567 5568 VerticalScrollbar verticalScrollBar; 5569 HorizontalScrollbar horizontalScrollBar; 5570 5571 this(ScrollableWidget sw, Widget parent) { 5572 this.sw = sw; 5573 5574 this.tabStop = false; 5575 5576 super(parent); 5577 5578 horizontalScrollBar = new HorizontalScrollbar(this); 5579 verticalScrollBar = new VerticalScrollbar(this); 5580 5581 horizontalScrollBar.showing_ = false; 5582 verticalScrollBar.showing_ = false; 5583 5584 horizontalScrollBar.addEventListener("scrolltonextline", { 5585 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5586 sw.horizontalScrollTo(horizontalScrollBar.position); 5587 }); 5588 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5589 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5590 sw.horizontalScrollTo(horizontalScrollBar.position); 5591 }); 5592 verticalScrollBar.addEventListener("scrolltonextline", { 5593 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5594 sw.verticalScrollTo(verticalScrollBar.position); 5595 }); 5596 verticalScrollBar.addEventListener("scrolltopreviousline", { 5597 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5598 sw.verticalScrollTo(verticalScrollBar.position); 5599 }); 5600 horizontalScrollBar.addEventListener("scrolltonextpage", { 5601 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5602 sw.horizontalScrollTo(horizontalScrollBar.position); 5603 }); 5604 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5605 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5606 sw.horizontalScrollTo(horizontalScrollBar.position); 5607 }); 5608 verticalScrollBar.addEventListener("scrolltonextpage", { 5609 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5610 sw.verticalScrollTo(verticalScrollBar.position); 5611 }); 5612 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5613 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5614 sw.verticalScrollTo(verticalScrollBar.position); 5615 }); 5616 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5617 horizontalScrollBar.setPosition(event.intValue); 5618 sw.horizontalScrollTo(horizontalScrollBar.position); 5619 }); 5620 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5621 verticalScrollBar.setPosition(event.intValue); 5622 sw.verticalScrollTo(verticalScrollBar.position); 5623 }); 5624 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5625 horizontalScrollBar.setPosition(event.intValue); 5626 sw.horizontalScrollTo(horizontalScrollBar.position); 5627 }); 5628 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5629 verticalScrollBar.setPosition(event.intValue); 5630 }); 5631 } 5632 5633 // this is supposed to be basically invisible... 5634 override int minWidth() { return sw.minWidth; } 5635 override int minHeight() { return sw.minHeight; } 5636 override int maxWidth() { return sw.maxWidth; } 5637 override int maxHeight() { return sw.maxHeight; } 5638 override int widthStretchiness() { return sw.widthStretchiness; } 5639 override int heightStretchiness() { return sw.heightStretchiness; } 5640 override int marginLeft() { return sw.marginLeft; } 5641 override int marginRight() { return sw.marginRight; } 5642 override int marginTop() { return sw.marginTop; } 5643 override int marginBottom() { return sw.marginBottom; } 5644 override int paddingLeft() { return sw.paddingLeft; } 5645 override int paddingRight() { return sw.paddingRight; } 5646 override int paddingTop() { return sw.paddingTop; } 5647 override int paddingBottom() { return sw.paddingBottom; } 5648 override void focus() { sw.focus(); } 5649 5650 5651 override void recomputeChildLayout() { 5652 // The stupid thing needs to calculate if a scroll bar is needed... 5653 recomputeChildLayoutHelper(); 5654 // then running it again will position things correctly if the bar is NOT needed 5655 recomputeChildLayoutHelper(); 5656 5657 // this sucks but meh it barely works 5658 } 5659 5660 private void recomputeChildLayoutHelper() { 5661 if(sw is null) return; 5662 5663 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5664 if(horizontalScrollBar && verticalScrollBar) { 5665 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5666 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5667 horizontalScrollBar.x = 0; 5668 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5669 5670 verticalScrollBar.width = verticalScrollBar.minWidth(); 5671 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5672 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5673 verticalScrollBar.y = 0 + 2; 5674 5675 sw.x = 0; 5676 sw.y = 0; 5677 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5678 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5679 5680 if(sw.contentWidth_ <= this.width) 5681 sw.scrollOrigin_.x = 0; 5682 if(sw.contentHeight_ <= this.height) 5683 sw.scrollOrigin_.y = 0; 5684 5685 horizontalScrollBar.recomputeChildLayout(); 5686 verticalScrollBar.recomputeChildLayout(); 5687 sw.recomputeChildLayout(); 5688 } 5689 5690 if(sw.contentWidth_ <= this.width) 5691 sw.scrollOrigin_.x = 0; 5692 if(sw.contentHeight_ <= this.height) 5693 sw.scrollOrigin_.y = 0; 5694 5695 if(sw.showingHorizontalScroll()) 5696 horizontalScrollBar.showing(true, false); 5697 else 5698 horizontalScrollBar.showing(false, false); 5699 if(sw.showingVerticalScroll()) 5700 verticalScrollBar.showing(true, false); 5701 else 5702 verticalScrollBar.showing(false, false); 5703 5704 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5705 verticalScrollBar.setMax(sw.contentHeight); 5706 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5707 5708 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5709 horizontalScrollBar.setMax(sw.contentWidth); 5710 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5711 } 5712 } 5713 5714 /* 5715 class ScrollableClientWidget : Widget { 5716 this(Widget parent) { 5717 super(parent); 5718 } 5719 override void paint(WidgetPainter p) { 5720 parent.paint(p); 5721 } 5722 } 5723 */ 5724 5725 /++ 5726 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. 5727 +/ 5728 abstract class Slider : Widget { 5729 this(int min, int max, int step, Widget parent) { 5730 min_ = min; 5731 max_ = max; 5732 step_ = step; 5733 page_ = step; 5734 super(parent); 5735 } 5736 5737 private int min_; 5738 private int max_; 5739 private int step_; 5740 private int position_; 5741 private int page_; 5742 5743 // selection start and selection end 5744 // tics 5745 // tooltip? 5746 // some way to see and just type the value 5747 // win32 buddy controls are labels 5748 5749 /// 5750 void setMin(int a) { 5751 min_ = a; 5752 version(custom_widgets) 5753 redraw(); 5754 version(win32_widgets) 5755 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5756 } 5757 /// 5758 int min() { 5759 return min_; 5760 } 5761 /// 5762 void setMax(int a) { 5763 max_ = a; 5764 version(custom_widgets) 5765 redraw(); 5766 version(win32_widgets) 5767 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5768 } 5769 /// 5770 int max() { 5771 return max_; 5772 } 5773 /// 5774 void setPosition(int a) { 5775 if(a > max) 5776 a = max; 5777 if(a < min) 5778 a = min; 5779 position_ = a; 5780 version(custom_widgets) 5781 setPositionCustom(a); 5782 5783 version(win32_widgets) 5784 setPositionWindows(a); 5785 } 5786 version(win32_widgets) { 5787 protected abstract void setPositionWindows(int a); 5788 } 5789 5790 protected abstract int win32direction(); 5791 5792 /++ 5793 Alias for [position] for better compatibility with generic code. 5794 5795 History: 5796 Added October 5, 2021 5797 +/ 5798 @property int value() { 5799 return position; 5800 } 5801 5802 /// 5803 int position() { 5804 return position_; 5805 } 5806 /// 5807 void setStep(int a) { 5808 step_ = a; 5809 version(win32_widgets) 5810 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5811 } 5812 /// 5813 int step() { 5814 return step_; 5815 } 5816 /// 5817 void setPageSize(int a) { 5818 page_ = a; 5819 version(win32_widgets) 5820 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5821 } 5822 /// 5823 int pageSize() { 5824 return page_; 5825 } 5826 5827 private void notify() { 5828 auto event = new ChangeEvent!int(this, &this.position); 5829 event.dispatch(); 5830 } 5831 5832 version(win32_widgets) 5833 void win32Setup(int style) { 5834 createWin32Window(this, TRACKBAR_CLASS, "", 5835 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5836 5837 // the trackbar sends the same messages as scroll, which 5838 // our other layer sends as these... just gonna translate 5839 // here 5840 this.addDirectEventListener("scrolltoposition", (Event event) { 5841 event.stopPropagation(); 5842 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5843 notify(); 5844 }); 5845 this.addDirectEventListener("scrolltonextline", (Event event) { 5846 event.stopPropagation(); 5847 this.setPosition(this.position + this.step_ * this.win32direction); 5848 notify(); 5849 }); 5850 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5851 event.stopPropagation(); 5852 this.setPosition(this.position - this.step_ * this.win32direction); 5853 notify(); 5854 }); 5855 this.addDirectEventListener("scrolltonextpage", (Event event) { 5856 event.stopPropagation(); 5857 this.setPosition(this.position + this.page_ * this.win32direction); 5858 notify(); 5859 }); 5860 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5861 event.stopPropagation(); 5862 this.setPosition(this.position - this.page_ * this.win32direction); 5863 notify(); 5864 }); 5865 5866 setMin(min_); 5867 setMax(max_); 5868 setStep(step_); 5869 setPageSize(page_); 5870 } 5871 5872 version(custom_widgets) { 5873 protected MouseTrackingWidget thumb; 5874 5875 protected abstract void setPositionCustom(int a); 5876 5877 override void defaultEventHandler_keydown(KeyDownEvent event) { 5878 switch(event.key) { 5879 case Key.Up: 5880 case Key.Right: 5881 setPosition(position() - step() * win32direction); 5882 changed(); 5883 break; 5884 case Key.Down: 5885 case Key.Left: 5886 setPosition(position() + step() * win32direction); 5887 changed(); 5888 break; 5889 case Key.Home: 5890 setPosition(win32direction > 0 ? min() : max()); 5891 changed(); 5892 break; 5893 case Key.End: 5894 setPosition(win32direction > 0 ? max() : min()); 5895 changed(); 5896 break; 5897 case Key.PageUp: 5898 setPosition(position() - pageSize() * win32direction); 5899 changed(); 5900 break; 5901 case Key.PageDown: 5902 setPosition(position() + pageSize() * win32direction); 5903 changed(); 5904 break; 5905 default: 5906 } 5907 super.defaultEventHandler_keydown(event); 5908 } 5909 5910 protected void changed() { 5911 auto ev = new ChangeEvent!int(this, &position); 5912 ev.dispatch(); 5913 } 5914 } 5915 } 5916 5917 /++ 5918 5919 +/ 5920 class VerticalSlider : Slider { 5921 this(int min, int max, int step, Widget parent) { 5922 version(custom_widgets) 5923 initialize(); 5924 5925 super(min, max, step, parent); 5926 5927 version(win32_widgets) 5928 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5929 } 5930 5931 protected override int win32direction() { 5932 return -1; 5933 } 5934 5935 version(win32_widgets) 5936 protected override void setPositionWindows(int a) { 5937 // the windows thing makes the top 0 and i don't like that. 5938 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5939 } 5940 5941 version(custom_widgets) 5942 private void initialize() { 5943 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5944 5945 thumb.tabStop = false; 5946 5947 thumb.thumbWidth = width; 5948 thumb.thumbHeight = scaleWithDpi(16); 5949 5950 thumb.addEventListener(EventType.change, () { 5951 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5952 sx = max - sx; 5953 //informProgramThatUserChangedPosition(sx); 5954 5955 position_ = sx; 5956 5957 changed(); 5958 }); 5959 } 5960 5961 version(custom_widgets) 5962 override void recomputeChildLayout() { 5963 thumb.thumbWidth = this.width; 5964 super.recomputeChildLayout(); 5965 setPositionCustom(position_); 5966 } 5967 5968 version(custom_widgets) 5969 protected override void setPositionCustom(int a) { 5970 if(max()) 5971 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5972 redraw(); 5973 } 5974 } 5975 5976 /++ 5977 5978 +/ 5979 class HorizontalSlider : Slider { 5980 this(int min, int max, int step, Widget parent) { 5981 version(custom_widgets) 5982 initialize(); 5983 5984 super(min, max, step, parent); 5985 5986 version(win32_widgets) 5987 win32Setup(TBS_HORZ); 5988 } 5989 5990 version(win32_widgets) 5991 protected override void setPositionWindows(int a) { 5992 SendMessage(hwnd, TBM_SETPOS, true, a); 5993 } 5994 5995 protected override int win32direction() { 5996 return 1; 5997 } 5998 5999 version(custom_widgets) 6000 private void initialize() { 6001 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 6002 6003 thumb.tabStop = false; 6004 6005 thumb.thumbWidth = scaleWithDpi(16); 6006 thumb.thumbHeight = height; 6007 6008 thumb.addEventListener(EventType.change, () { 6009 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6010 //informProgramThatUserChangedPosition(sx); 6011 6012 position_ = sx; 6013 6014 changed(); 6015 }); 6016 } 6017 6018 version(custom_widgets) 6019 override void recomputeChildLayout() { 6020 thumb.thumbHeight = this.height; 6021 super.recomputeChildLayout(); 6022 setPositionCustom(position_); 6023 } 6024 6025 version(custom_widgets) 6026 protected override void setPositionCustom(int a) { 6027 if(max()) 6028 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6029 redraw(); 6030 } 6031 } 6032 6033 6034 /// 6035 abstract class ScrollbarBase : Widget { 6036 /// 6037 this(Widget parent) { 6038 super(parent); 6039 tabStop = false; 6040 step_ = scaleWithDpi(16); 6041 } 6042 6043 private int viewableArea_; 6044 private int max_; 6045 private int step_;// = 16; 6046 private int position_; 6047 6048 /// 6049 bool atEnd() { 6050 return position_ + viewableArea_ >= max_; 6051 } 6052 6053 /// 6054 bool atStart() { 6055 return position_ == 0; 6056 } 6057 6058 /// 6059 void setViewableArea(int a) { 6060 viewableArea_ = a; 6061 version(custom_widgets) 6062 redraw(); 6063 } 6064 /// 6065 void setMax(int a) { 6066 max_ = a; 6067 version(custom_widgets) 6068 redraw(); 6069 } 6070 /// 6071 int max() { 6072 return max_; 6073 } 6074 /// 6075 void setPosition(int a) { 6076 auto logicalMax = max_ - viewableArea_; 6077 if(a == int.max) 6078 a = logicalMax; 6079 6080 if(a > logicalMax) 6081 a = logicalMax; 6082 if(a < 0) 6083 a = 0; 6084 6085 position_ = a; 6086 6087 version(custom_widgets) 6088 redraw(); 6089 } 6090 /// 6091 int position() { 6092 return position_; 6093 } 6094 /// 6095 void setStep(int a) { 6096 step_ = a; 6097 } 6098 /// 6099 int step() { 6100 return step_; 6101 } 6102 6103 // FIXME: remove this.... maybe 6104 /+ 6105 protected void informProgramThatUserChangedPosition(int n) { 6106 position_ = n; 6107 auto evt = new Event(EventType.change, this); 6108 evt.intValue = n; 6109 evt.dispatch(); 6110 } 6111 +/ 6112 6113 version(custom_widgets) { 6114 enum MIN_THUMB_SIZE = 8; 6115 6116 abstract protected int getBarDim(); 6117 int thumbSize() { 6118 if(viewableArea_ >= max_ || max_ == 0) 6119 return getBarDim(); 6120 6121 int res = viewableArea_ * getBarDim() / max_; 6122 6123 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6124 res = scaleWithDpi(MIN_THUMB_SIZE); 6125 6126 return res; 6127 } 6128 6129 int thumbPosition() { 6130 /* 6131 viewableArea_ is the viewport height/width 6132 position_ is where we are 6133 */ 6134 //if(position_ + viewableArea_ >= max_) 6135 //return getBarDim - thumbSize; 6136 6137 auto maximumPossibleValue = getBarDim() - thumbSize; 6138 auto maximiumLogicalValue = max_ - viewableArea_; 6139 6140 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6141 6142 return p; 6143 } 6144 } 6145 } 6146 6147 //public import mgt; 6148 6149 /++ 6150 A mouse tracking widget is one that follows the mouse when dragged inside it. 6151 6152 Concrete subclasses may include a scrollbar thumb and a volume control. 6153 +/ 6154 //version(custom_widgets) 6155 class MouseTrackingWidget : Widget { 6156 6157 /// 6158 int positionX() { return positionX_; } 6159 /// 6160 int positionY() { return positionY_; } 6161 6162 /// 6163 void positionX(int p) { positionX_ = p; } 6164 /// 6165 void positionY(int p) { positionY_ = p; } 6166 6167 private int positionX_; 6168 private int positionY_; 6169 6170 /// 6171 enum Orientation { 6172 horizontal, /// 6173 vertical, /// 6174 twoDimensional, /// 6175 } 6176 6177 private int thumbWidth_; 6178 private int thumbHeight_; 6179 6180 /// 6181 int thumbWidth() { return thumbWidth_; } 6182 /// 6183 int thumbHeight() { return thumbHeight_; } 6184 /// 6185 int thumbWidth(int a) { return thumbWidth_ = a; } 6186 /// 6187 int thumbHeight(int a) { return thumbHeight_ = a; } 6188 6189 private bool dragging; 6190 private bool hovering; 6191 private int startMouseX, startMouseY; 6192 6193 /// 6194 this(Orientation orientation, Widget parent) { 6195 super(parent); 6196 6197 //assert(parentWindow !is null); 6198 6199 addEventListener((MouseDownEvent event) { 6200 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6201 dragging = true; 6202 startMouseX = event.clientX - positionX; 6203 startMouseY = event.clientY - positionY; 6204 parentWindow.captureMouse(this); 6205 } else { 6206 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6207 positionX = event.clientX - thumbWidth / 2; 6208 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6209 positionY = event.clientY - thumbHeight / 2; 6210 6211 if(positionX + thumbWidth > this.width) 6212 positionX = this.width - thumbWidth; 6213 if(positionY + thumbHeight > this.height) 6214 positionY = this.height - thumbHeight; 6215 6216 if(positionX < 0) 6217 positionX = 0; 6218 if(positionY < 0) 6219 positionY = 0; 6220 6221 6222 // this.emit!(ChangeEvent!void)(); 6223 auto evt = new Event(EventType.change, this); 6224 evt.sendDirectly(); 6225 6226 redraw(); 6227 6228 } 6229 }); 6230 6231 addEventListener(EventType.mouseup, (Event event) { 6232 dragging = false; 6233 parentWindow.releaseMouseCapture(); 6234 }); 6235 6236 addEventListener(EventType.mouseout, (Event event) { 6237 if(!hovering) 6238 return; 6239 hovering = false; 6240 redraw(); 6241 }); 6242 6243 int lpx, lpy; 6244 6245 addEventListener((MouseMoveEvent event) { 6246 auto oh = hovering; 6247 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6248 hovering = true; 6249 } else { 6250 hovering = false; 6251 } 6252 if(!dragging) { 6253 if(hovering != oh) 6254 redraw(); 6255 return; 6256 } 6257 6258 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6259 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6260 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6261 positionY = event.clientY - startMouseY; 6262 6263 if(positionX + thumbWidth > this.width) 6264 positionX = this.width - thumbWidth; 6265 if(positionY + thumbHeight > this.height) 6266 positionY = this.height - thumbHeight; 6267 6268 if(positionX < 0) 6269 positionX = 0; 6270 if(positionY < 0) 6271 positionY = 0; 6272 6273 if(positionX != lpx || positionY != lpy) { 6274 lpx = positionX; 6275 lpy = positionY; 6276 6277 auto evt = new Event(EventType.change, this); 6278 evt.sendDirectly(); 6279 } 6280 6281 redraw(); 6282 }); 6283 } 6284 6285 version(custom_widgets) 6286 override void paint(WidgetPainter painter) { 6287 auto cs = getComputedStyle(); 6288 auto c = darken(cs.windowBackgroundColor, 0.2); 6289 painter.outlineColor = c; 6290 painter.fillColor = c; 6291 painter.drawRectangle(Point(0, 0), this.width, this.height); 6292 6293 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6294 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6295 } 6296 } 6297 6298 //version(custom_widgets) 6299 //private 6300 class HorizontalScrollbar : ScrollbarBase { 6301 6302 version(custom_widgets) { 6303 private MouseTrackingWidget thumb; 6304 6305 override int getBarDim() { 6306 return thumb.width; 6307 } 6308 } 6309 6310 override void setViewableArea(int a) { 6311 super.setViewableArea(a); 6312 6313 version(win32_widgets) { 6314 SCROLLINFO info; 6315 info.cbSize = info.sizeof; 6316 info.nPage = a + 1; 6317 info.fMask = SIF_PAGE; 6318 SetScrollInfo(hwnd, SB_CTL, &info, true); 6319 } else version(custom_widgets) { 6320 thumb.positionX = thumbPosition; 6321 thumb.thumbWidth = thumbSize; 6322 thumb.redraw(); 6323 } else static assert(0); 6324 6325 } 6326 6327 override void setMax(int a) { 6328 super.setMax(a); 6329 version(win32_widgets) { 6330 SCROLLINFO info; 6331 info.cbSize = info.sizeof; 6332 info.nMin = 0; 6333 info.nMax = max; 6334 info.fMask = SIF_RANGE; 6335 SetScrollInfo(hwnd, SB_CTL, &info, true); 6336 } else version(custom_widgets) { 6337 thumb.positionX = thumbPosition; 6338 thumb.thumbWidth = thumbSize; 6339 thumb.redraw(); 6340 } 6341 } 6342 6343 override void setPosition(int a) { 6344 super.setPosition(a); 6345 version(win32_widgets) { 6346 SCROLLINFO info; 6347 info.cbSize = info.sizeof; 6348 info.fMask = SIF_POS; 6349 info.nPos = position; 6350 SetScrollInfo(hwnd, SB_CTL, &info, true); 6351 } else version(custom_widgets) { 6352 thumb.positionX = thumbPosition(); 6353 thumb.thumbWidth = thumbSize; 6354 thumb.redraw(); 6355 } else static assert(0); 6356 } 6357 6358 this(Widget parent) { 6359 super(parent); 6360 6361 version(win32_widgets) { 6362 createWin32Window(this, "Scrollbar"w, "", 6363 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6364 } else version(custom_widgets) { 6365 auto vl = new HorizontalLayout(this); 6366 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6367 leftButton.setClickRepeat(scrollClickRepeatInterval); 6368 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6369 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6370 rightButton.setClickRepeat(scrollClickRepeatInterval); 6371 6372 leftButton.tabStop = false; 6373 rightButton.tabStop = false; 6374 thumb.tabStop = false; 6375 6376 leftButton.addEventListener(EventType.triggered, () { 6377 this.emitCommand!"scrolltopreviousline"(); 6378 //informProgramThatUserChangedPosition(position - step()); 6379 }); 6380 rightButton.addEventListener(EventType.triggered, () { 6381 this.emitCommand!"scrolltonextline"(); 6382 //informProgramThatUserChangedPosition(position + step()); 6383 }); 6384 6385 thumb.thumbWidth = this.minWidth; 6386 thumb.thumbHeight = scaleWithDpi(16); 6387 6388 thumb.addEventListener(EventType.change, () { 6389 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6390 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6391 6392 //informProgramThatUserChangedPosition(sx); 6393 6394 auto ev = new ScrollToPositionEvent(this, sx); 6395 ev.dispatch(); 6396 }); 6397 } 6398 } 6399 6400 override int minHeight() { return scaleWithDpi(16); } 6401 override int maxHeight() { return scaleWithDpi(16); } 6402 override int minWidth() { return scaleWithDpi(48); } 6403 } 6404 6405 class ScrollToPositionEvent : Event { 6406 enum EventString = "scrolltoposition"; 6407 6408 this(Widget target, int value) { 6409 this.value = value; 6410 super(EventString, target); 6411 } 6412 6413 immutable int value; 6414 6415 override @property int intValue() { 6416 return value; 6417 } 6418 } 6419 6420 //version(custom_widgets) 6421 //private 6422 class VerticalScrollbar : ScrollbarBase { 6423 6424 version(custom_widgets) { 6425 override int getBarDim() { 6426 return thumb.height; 6427 } 6428 6429 private MouseTrackingWidget thumb; 6430 } 6431 6432 override void setViewableArea(int a) { 6433 super.setViewableArea(a); 6434 6435 version(win32_widgets) { 6436 SCROLLINFO info; 6437 info.cbSize = info.sizeof; 6438 info.nPage = a + 1; 6439 info.fMask = SIF_PAGE; 6440 SetScrollInfo(hwnd, SB_CTL, &info, true); 6441 } else version(custom_widgets) { 6442 thumb.positionY = thumbPosition; 6443 thumb.thumbHeight = thumbSize; 6444 thumb.redraw(); 6445 } else static assert(0); 6446 6447 } 6448 6449 override void setMax(int a) { 6450 super.setMax(a); 6451 version(win32_widgets) { 6452 SCROLLINFO info; 6453 info.cbSize = info.sizeof; 6454 info.nMin = 0; 6455 info.nMax = max; 6456 info.fMask = SIF_RANGE; 6457 SetScrollInfo(hwnd, SB_CTL, &info, true); 6458 } else version(custom_widgets) { 6459 thumb.positionY = thumbPosition; 6460 thumb.thumbHeight = thumbSize; 6461 thumb.redraw(); 6462 } 6463 } 6464 6465 override void setPosition(int a) { 6466 super.setPosition(a); 6467 version(win32_widgets) { 6468 SCROLLINFO info; 6469 info.cbSize = info.sizeof; 6470 info.fMask = SIF_POS; 6471 info.nPos = position; 6472 SetScrollInfo(hwnd, SB_CTL, &info, true); 6473 } else version(custom_widgets) { 6474 thumb.positionY = thumbPosition; 6475 thumb.thumbHeight = thumbSize; 6476 thumb.redraw(); 6477 } else static assert(0); 6478 } 6479 6480 this(Widget parent) { 6481 super(parent); 6482 6483 version(win32_widgets) { 6484 createWin32Window(this, "Scrollbar"w, "", 6485 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6486 } else version(custom_widgets) { 6487 auto vl = new VerticalLayout(this); 6488 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6489 upButton.setClickRepeat(scrollClickRepeatInterval); 6490 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6491 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6492 downButton.setClickRepeat(scrollClickRepeatInterval); 6493 6494 upButton.addEventListener(EventType.triggered, () { 6495 this.emitCommand!"scrolltopreviousline"(); 6496 //informProgramThatUserChangedPosition(position - step()); 6497 }); 6498 downButton.addEventListener(EventType.triggered, () { 6499 this.emitCommand!"scrolltonextline"(); 6500 //informProgramThatUserChangedPosition(position + step()); 6501 }); 6502 6503 thumb.thumbWidth = this.minWidth; 6504 thumb.thumbHeight = scaleWithDpi(16); 6505 6506 thumb.addEventListener(EventType.change, () { 6507 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6508 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6509 6510 auto ev = new ScrollToPositionEvent(this, sy); 6511 ev.dispatch(); 6512 6513 //informProgramThatUserChangedPosition(sy); 6514 }); 6515 6516 upButton.tabStop = false; 6517 downButton.tabStop = false; 6518 thumb.tabStop = false; 6519 } 6520 } 6521 6522 override int minWidth() { return scaleWithDpi(16); } 6523 override int maxWidth() { return scaleWithDpi(16); } 6524 override int minHeight() { return scaleWithDpi(48); } 6525 } 6526 6527 6528 /++ 6529 EXPERIMENTAL 6530 6531 A widget specialized for being a container for other widgets. 6532 6533 History: 6534 Added May 29, 2021. Not stabilized at this time. 6535 +/ 6536 class WidgetContainer : Widget { 6537 this(Widget parent) { 6538 tabStop = false; 6539 super(parent); 6540 } 6541 6542 override int maxHeight() { 6543 if(this.children.length == 1) { 6544 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6545 } else { 6546 return int.max; 6547 } 6548 } 6549 6550 override int maxWidth() { 6551 if(this.children.length == 1) { 6552 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6553 } else { 6554 return int.max; 6555 } 6556 } 6557 6558 /+ 6559 6560 override int minHeight() { 6561 int largest = 0; 6562 int margins = 0; 6563 int lastMargin = 0; 6564 foreach(child; children) { 6565 auto mh = child.minHeight(); 6566 if(mh > largest) 6567 largest = mh; 6568 margins += mymax(lastMargin, child.marginTop()); 6569 lastMargin = child.marginBottom(); 6570 } 6571 return largest + margins; 6572 } 6573 6574 override int maxHeight() { 6575 int largest = 0; 6576 int margins = 0; 6577 int lastMargin = 0; 6578 foreach(child; children) { 6579 auto mh = child.maxHeight(); 6580 if(mh == int.max) 6581 return int.max; 6582 if(mh > largest) 6583 largest = mh; 6584 margins += mymax(lastMargin, child.marginTop()); 6585 lastMargin = child.marginBottom(); 6586 } 6587 return largest + margins; 6588 } 6589 6590 override int minWidth() { 6591 int min; 6592 foreach(child; children) { 6593 auto cm = child.minWidth; 6594 if(cm > min) 6595 min = cm; 6596 } 6597 return min + paddingLeft + paddingRight; 6598 } 6599 6600 override int minHeight() { 6601 int min; 6602 foreach(child; children) { 6603 auto cm = child.minHeight; 6604 if(cm > min) 6605 min = cm; 6606 } 6607 return min + paddingTop + paddingBottom; 6608 } 6609 6610 override int maxHeight() { 6611 int largest = 0; 6612 int margins = 0; 6613 int lastMargin = 0; 6614 foreach(child; children) { 6615 auto mh = child.maxHeight(); 6616 if(mh == int.max) 6617 return int.max; 6618 if(mh > largest) 6619 largest = mh; 6620 margins += mymax(lastMargin, child.marginTop()); 6621 lastMargin = child.marginBottom(); 6622 } 6623 return largest + margins; 6624 } 6625 6626 override int heightStretchiness() { 6627 int max; 6628 foreach(child; children) { 6629 auto c = child.heightStretchiness; 6630 if(c > max) 6631 max = c; 6632 } 6633 return max; 6634 } 6635 6636 override int marginTop() { 6637 if(this.children.length) 6638 return this.children[0].marginTop; 6639 return 0; 6640 } 6641 +/ 6642 } 6643 6644 /// 6645 abstract class Layout : Widget { 6646 this(Widget parent) { 6647 tabStop = false; 6648 super(parent); 6649 } 6650 } 6651 6652 /++ 6653 Makes all children minimum width and height, placing them down 6654 left to right, top to bottom. 6655 6656 Useful if you want to make a list of buttons that automatically 6657 wrap to a new line when necessary. 6658 +/ 6659 class InlineBlockLayout : Layout { 6660 /// 6661 this(Widget parent) { super(parent); } 6662 6663 override void recomputeChildLayout() { 6664 registerMovement(); 6665 6666 int x = this.paddingLeft, y = this.paddingTop; 6667 6668 int lineHeight; 6669 int previousMargin = 0; 6670 int previousMarginBottom = 0; 6671 6672 foreach(child; children) { 6673 if(child.hidden) 6674 continue; 6675 if(cast(FixedPosition) child) { 6676 child.recomputeChildLayout(); 6677 continue; 6678 } 6679 child.width = child.flexBasisWidth(); 6680 if(child.width == 0) 6681 child.width = child.minWidth(); 6682 if(child.width == 0) 6683 child.width = 32; 6684 6685 child.height = child.flexBasisHeight(); 6686 if(child.height == 0) 6687 child.height = child.minHeight(); 6688 if(child.height == 0) 6689 child.height = 32; 6690 6691 if(x + child.width + paddingRight > this.width) { 6692 x = this.paddingLeft; 6693 y += lineHeight; 6694 lineHeight = 0; 6695 previousMargin = 0; 6696 previousMarginBottom = 0; 6697 } 6698 6699 auto margin = child.marginLeft; 6700 if(previousMargin > margin) 6701 margin = previousMargin; 6702 6703 x += margin; 6704 6705 child.x = x; 6706 child.y = y; 6707 6708 int marginTopApplied; 6709 if(child.marginTop > previousMarginBottom) { 6710 child.y += child.marginTop; 6711 marginTopApplied = child.marginTop; 6712 } 6713 6714 x += child.width; 6715 previousMargin = child.marginRight; 6716 6717 if(child.marginBottom > previousMarginBottom) 6718 previousMarginBottom = child.marginBottom; 6719 6720 auto h = child.height + previousMarginBottom + marginTopApplied; 6721 if(h > lineHeight) 6722 lineHeight = h; 6723 6724 child.recomputeChildLayout(); 6725 } 6726 6727 } 6728 6729 override int minWidth() { 6730 int min; 6731 foreach(child; children) { 6732 auto cm = child.minWidth; 6733 if(cm > min) 6734 min = cm; 6735 } 6736 return min + paddingLeft + paddingRight; 6737 } 6738 6739 override int minHeight() { 6740 int min; 6741 foreach(child; children) { 6742 auto cm = child.minHeight; 6743 if(cm > min) 6744 min = cm; 6745 } 6746 return min + paddingTop + paddingBottom; 6747 } 6748 } 6749 6750 /++ 6751 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6752 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6753 the [TabWidget] will automatically change pages of child widgets. 6754 6755 This allows you to react to it however you see fit rather than having to 6756 be tied to just the new sets of child widgets. 6757 6758 It sends the message in the form of `this.emitCommand!"changetab"();`. 6759 6760 History: 6761 Added December 24, 2021 (dub v10.5) 6762 +/ 6763 class TabMessageWidget : Widget { 6764 6765 protected void tabIndexClicked(int item) { 6766 this.emitCommand!"changetab"(); 6767 } 6768 6769 /++ 6770 Adds the a new tab to the control with the given title. 6771 6772 Returns: 6773 The index of the newly added tab. You will need to know 6774 this index to refer to it later and to know which tab to 6775 change to when you get a changetab message. 6776 +/ 6777 int addTab(string title, int pos = int.max) { 6778 version(win32_widgets) { 6779 TCITEM item; 6780 item.mask = TCIF_TEXT; 6781 WCharzBuffer buf = WCharzBuffer(title); 6782 item.pszText = buf.ptr; 6783 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6784 } else version(custom_widgets) { 6785 if(pos >= tabs.length) { 6786 tabs ~= title; 6787 redraw(); 6788 return cast(int) tabs.length - 1; 6789 } else if(pos <= 0) { 6790 tabs = title ~ tabs; 6791 redraw(); 6792 return 0; 6793 } else { 6794 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6795 redraw(); 6796 return pos; 6797 } 6798 } 6799 } 6800 6801 override void addChild(Widget child, int pos = int.max) { 6802 if(container) 6803 container.addChild(child, pos); 6804 else 6805 super.addChild(child, pos); 6806 } 6807 6808 protected Widget makeContainer() { 6809 return new Widget(this); 6810 } 6811 6812 private Widget container; 6813 6814 override void recomputeChildLayout() { 6815 version(win32_widgets) { 6816 this.registerMovement(); 6817 6818 RECT rect; 6819 GetWindowRect(hwnd, &rect); 6820 6821 auto left = rect.left; 6822 auto top = rect.top; 6823 6824 TabCtrl_AdjustRect(hwnd, false, &rect); 6825 foreach(child; children) { 6826 if(!child.showing) continue; 6827 child.x = rect.left - left; 6828 child.y = rect.top - top; 6829 child.width = rect.right - rect.left; 6830 child.height = rect.bottom - rect.top; 6831 child.recomputeChildLayout(); 6832 } 6833 } else version(custom_widgets) { 6834 this.registerMovement(); 6835 foreach(child; children) { 6836 if(!child.showing) continue; 6837 child.x = 2; 6838 child.y = tabBarHeight + 2; // for the border 6839 child.width = width - 4; // for the border 6840 child.height = height - tabBarHeight - 2 - 2; // for the border 6841 child.recomputeChildLayout(); 6842 } 6843 } else static assert(0); 6844 } 6845 6846 version(custom_widgets) 6847 string[] tabs; 6848 6849 this(Widget parent) { 6850 super(parent); 6851 6852 tabStop = false; 6853 6854 version(win32_widgets) { 6855 createWin32Window(this, WC_TABCONTROL, "", 0); 6856 } else version(custom_widgets) { 6857 addEventListener((ClickEvent event) { 6858 if(event.target !is this) 6859 return; 6860 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 6861 auto t = (event.clientX / tabWidth); 6862 if(t >= 0 && t < tabs.length) { 6863 currentTab_ = t; 6864 tabIndexClicked(t); 6865 redraw(); 6866 } 6867 } 6868 }); 6869 } else static assert(0); 6870 6871 this.container = makeContainer(); 6872 } 6873 6874 override int marginTop() { return 4; } 6875 override int paddingBottom() { return 4; } 6876 6877 override int minHeight() { 6878 int max = 0; 6879 foreach(child; children) 6880 max = mymax(child.minHeight, max); 6881 6882 6883 version(win32_widgets) { 6884 RECT rect; 6885 rect.right = this.width; 6886 rect.bottom = max; 6887 TabCtrl_AdjustRect(hwnd, true, &rect); 6888 6889 max = rect.bottom; 6890 } else { 6891 max += defaultLineHeight + 4; 6892 } 6893 6894 6895 return max; 6896 } 6897 6898 version(win32_widgets) 6899 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6900 switch(code) { 6901 case TCN_SELCHANGE: 6902 auto sel = TabCtrl_GetCurSel(hwnd); 6903 tabIndexClicked(sel); 6904 break; 6905 default: 6906 } 6907 return 0; 6908 } 6909 6910 version(custom_widgets) { 6911 private int currentTab_; 6912 private int tabBarHeight() { return defaultLineHeight; } 6913 int tabWidth() { return scaleWithDpi(80); } 6914 } 6915 6916 version(win32_widgets) 6917 override void paint(WidgetPainter painter) {} 6918 6919 version(custom_widgets) 6920 override void paint(WidgetPainter painter) { 6921 auto cs = getComputedStyle(); 6922 6923 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6924 6925 int posX = 0; 6926 foreach(idx, title; tabs) { 6927 auto isCurrent = idx == getCurrentTab(); 6928 6929 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6930 6931 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6932 painter.outlineColor = cs.foregroundColor; 6933 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6934 6935 if(isCurrent) { 6936 painter.outlineColor = cs.windowBackgroundColor; 6937 painter.fillColor = Color.transparent; 6938 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6939 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6940 6941 painter.outlineColor = Color.white; 6942 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6943 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6944 painter.outlineColor = cs.activeTabColor; 6945 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6946 } 6947 6948 posX += tabWidth - 2; 6949 } 6950 } 6951 6952 /// 6953 @scriptable 6954 void setCurrentTab(int item) { 6955 version(win32_widgets) 6956 TabCtrl_SetCurSel(hwnd, item); 6957 else version(custom_widgets) 6958 currentTab_ = item; 6959 else static assert(0); 6960 6961 tabIndexClicked(item); 6962 } 6963 6964 /// 6965 @scriptable 6966 int getCurrentTab() { 6967 version(win32_widgets) 6968 return TabCtrl_GetCurSel(hwnd); 6969 else version(custom_widgets) 6970 return currentTab_; // FIXME 6971 else static assert(0); 6972 } 6973 6974 /// 6975 @scriptable 6976 void removeTab(int item) { 6977 if(item && item == getCurrentTab()) 6978 setCurrentTab(item - 1); 6979 6980 version(win32_widgets) { 6981 TabCtrl_DeleteItem(hwnd, item); 6982 } 6983 6984 for(int a = item; a < children.length - 1; a++) 6985 this._children[a] = this._children[a + 1]; 6986 this._children = this._children[0 .. $-1]; 6987 } 6988 6989 } 6990 6991 6992 /++ 6993 A tab widget is a set of clickable tab buttons followed by a content area. 6994 6995 6996 Tabs can change existing content or can be new pages. 6997 6998 When the user picks a different tab, a `change` message is generated. 6999 +/ 7000 class TabWidget : TabMessageWidget { 7001 this(Widget parent) { 7002 super(parent); 7003 } 7004 7005 override protected Widget makeContainer() { 7006 return null; 7007 } 7008 7009 override void addChild(Widget child, int pos = int.max) { 7010 if(auto twp = cast(TabWidgetPage) child) { 7011 Widget.addChild(child, pos); 7012 if(pos == int.max) 7013 pos = cast(int) this.children.length - 1; 7014 7015 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7016 7017 if(pos != getCurrentTab) { 7018 child.showing = false; 7019 } 7020 } else { 7021 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7022 } 7023 } 7024 7025 // FIXME: add tab icons at some point, Windows supports them 7026 /++ 7027 Adds a page and its associated tab with the given label to the widget. 7028 7029 Returns: 7030 The added page object, to which you can add other widgets. 7031 +/ 7032 @scriptable 7033 TabWidgetPage addPage(string title) { 7034 return new TabWidgetPage(title, this); 7035 } 7036 7037 /++ 7038 Gets the page at the given tab index, or `null` if the index is bad. 7039 7040 History: 7041 Added December 24, 2021. 7042 +/ 7043 TabWidgetPage getPage(int index) { 7044 if(index < this.children.length) 7045 return null; 7046 return cast(TabWidgetPage) this.children[index]; 7047 } 7048 7049 /++ 7050 While you can still use the addTab from the parent class, 7051 *strongly* recommend you use [addPage] insteaad. 7052 7053 History: 7054 Added December 24, 2021 to fulful the interface 7055 requirement that came from adding [TabMessageWidget]. 7056 7057 You should not use it though since the [addPage] function 7058 is much easier to use here. 7059 +/ 7060 override int addTab(string title, int pos = int.max) { 7061 auto p = addPage(title); 7062 foreach(idx, child; this.children) 7063 if(child is p) 7064 return cast(int) idx; 7065 return -1; 7066 } 7067 7068 protected override void tabIndexClicked(int item) { 7069 foreach(idx, child; children) { 7070 child.showing(false, false); // batch the recalculates for the end 7071 } 7072 7073 foreach(idx, child; children) { 7074 if(idx == item) { 7075 child.showing(true, false); 7076 if(parentWindow) { 7077 auto f = parentWindow.getFirstFocusable(child); 7078 if(f) 7079 f.focus(); 7080 } 7081 recomputeChildLayout(); 7082 } 7083 } 7084 7085 version(win32_widgets) { 7086 InvalidateRect(hwnd, null, true); 7087 } else version(custom_widgets) { 7088 this.redraw(); 7089 } 7090 } 7091 7092 } 7093 7094 /++ 7095 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7096 7097 You add [TabWidgetPage]s to it. 7098 +/ 7099 class PageWidget : Widget { 7100 this(Widget parent) { 7101 super(parent); 7102 } 7103 7104 override int minHeight() { 7105 int max = 0; 7106 foreach(child; children) 7107 max = mymax(child.minHeight, max); 7108 7109 return max; 7110 } 7111 7112 7113 override void addChild(Widget child, int pos = int.max) { 7114 if(auto twp = cast(TabWidgetPage) child) { 7115 super.addChild(child, pos); 7116 if(pos == int.max) 7117 pos = cast(int) this.children.length - 1; 7118 7119 if(pos != getCurrentTab) { 7120 child.showing = false; 7121 } 7122 } else { 7123 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7124 } 7125 } 7126 7127 override void recomputeChildLayout() { 7128 this.registerMovement(); 7129 foreach(child; children) { 7130 child.x = 0; 7131 child.y = 0; 7132 child.width = width; 7133 child.height = height; 7134 child.recomputeChildLayout(); 7135 } 7136 } 7137 7138 private int currentTab_; 7139 7140 /// 7141 @scriptable 7142 void setCurrentTab(int item) { 7143 currentTab_ = item; 7144 7145 showOnly(item); 7146 } 7147 7148 /// 7149 @scriptable 7150 int getCurrentTab() { 7151 return currentTab_; 7152 } 7153 7154 /// 7155 @scriptable 7156 void removeTab(int item) { 7157 if(item && item == getCurrentTab()) 7158 setCurrentTab(item - 1); 7159 7160 for(int a = item; a < children.length - 1; a++) 7161 this._children[a] = this._children[a + 1]; 7162 this._children = this._children[0 .. $-1]; 7163 } 7164 7165 /// 7166 @scriptable 7167 TabWidgetPage addPage(string title) { 7168 return new TabWidgetPage(title, this); 7169 } 7170 7171 private void showOnly(int item) { 7172 foreach(idx, child; children) 7173 if(idx == item) { 7174 child.show(); 7175 child.recomputeChildLayout(); 7176 } else { 7177 child.hide(); 7178 } 7179 } 7180 7181 } 7182 7183 /++ 7184 7185 +/ 7186 class TabWidgetPage : Widget { 7187 string title; 7188 this(string title, Widget parent) { 7189 this.title = title; 7190 this.tabStop = false; 7191 super(parent); 7192 7193 ///* 7194 version(win32_widgets) { 7195 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7196 } 7197 //*/ 7198 } 7199 7200 override int minHeight() { 7201 int sum = 0; 7202 foreach(child; children) 7203 sum += child.minHeight(); 7204 return sum; 7205 } 7206 } 7207 7208 version(none) 7209 /++ 7210 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7211 7212 I think I need to modify the layout algorithms to support this. 7213 +/ 7214 class CollapsableSidebar : Widget { 7215 7216 } 7217 7218 /// Stacks the widgets vertically, taking all the available width for each child. 7219 class VerticalLayout : Layout { 7220 // most of this is intentionally blank - widget's default is vertical layout right now 7221 /// 7222 this(Widget parent) { super(parent); } 7223 7224 /++ 7225 Sets a max width for the layout so you don't have to subclass. The max width 7226 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7227 7228 History: 7229 Added November 29, 2021 (dub v10.5) 7230 +/ 7231 this(int maxWidth, Widget parent) { 7232 this.mw = maxWidth; 7233 super(parent); 7234 } 7235 7236 private int mw = int.max; 7237 7238 override int maxWidth() { return scaleWithDpi(mw); } 7239 } 7240 7241 /// Stacks the widgets horizontally, taking all the available height for each child. 7242 class HorizontalLayout : Layout { 7243 /// 7244 this(Widget parent) { super(parent); } 7245 7246 /++ 7247 Sets a max height for the layout so you don't have to subclass. The max height 7248 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7249 7250 History: 7251 Added November 29, 2021 (dub v10.5) 7252 +/ 7253 this(int maxHeight, Widget parent) { 7254 this.mh = maxHeight; 7255 super(parent); 7256 } 7257 7258 private int mh = 0; 7259 7260 7261 7262 override void recomputeChildLayout() { 7263 .recomputeChildLayout!"width"(this); 7264 } 7265 7266 override int minHeight() { 7267 int largest = 0; 7268 int margins = 0; 7269 int lastMargin = 0; 7270 foreach(child; children) { 7271 auto mh = child.minHeight(); 7272 if(mh > largest) 7273 largest = mh; 7274 margins += mymax(lastMargin, child.marginTop()); 7275 lastMargin = child.marginBottom(); 7276 } 7277 return largest + margins; 7278 } 7279 7280 override int maxHeight() { 7281 if(mh != 0) 7282 return mymax(minHeight, scaleWithDpi(mh)); 7283 7284 int largest = 0; 7285 int margins = 0; 7286 int lastMargin = 0; 7287 foreach(child; children) { 7288 auto mh = child.maxHeight(); 7289 if(mh == int.max) 7290 return int.max; 7291 if(mh > largest) 7292 largest = mh; 7293 margins += mymax(lastMargin, child.marginTop()); 7294 lastMargin = child.marginBottom(); 7295 } 7296 return largest + margins; 7297 } 7298 7299 override int heightStretchiness() { 7300 int max; 7301 foreach(child; children) { 7302 auto c = child.heightStretchiness; 7303 if(c > max) 7304 max = c; 7305 } 7306 return max; 7307 } 7308 7309 } 7310 7311 version(win32_widgets) 7312 private 7313 extern(Windows) 7314 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7315 Widget* pwin = hwnd in Widget.nativeMapping; 7316 if(pwin is null) 7317 return DefWindowProc(hwnd, message, wparam, lparam); 7318 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7319 if(win is null) 7320 return DefWindowProc(hwnd, message, wparam, lparam); 7321 7322 switch(message) { 7323 case WM_SIZE: 7324 auto width = LOWORD(lparam); 7325 auto height = HIWORD(lparam); 7326 7327 auto hdc = GetDC(hwnd); 7328 auto hdcBmp = CreateCompatibleDC(hdc); 7329 7330 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7331 if(width > win.bmpWidth || height > win.bmpHeight) { 7332 auto oldBuffer = win.buffer; 7333 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7334 7335 if(oldBuffer) 7336 DeleteObject(oldBuffer); 7337 7338 win.bmpWidth = width; 7339 win.bmpHeight = height; 7340 } 7341 7342 // just always erase it upon resizing so minigui can draw over with a clean slate 7343 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7344 7345 auto brush = GetSysColorBrush(COLOR_3DFACE); 7346 RECT r; 7347 r.left = 0; 7348 r.top = 0; 7349 r.right = width; 7350 r.bottom = height; 7351 FillRect(hdcBmp, &r, brush); 7352 7353 SelectObject(hdcBmp, oldBmp); 7354 DeleteDC(hdcBmp); 7355 ReleaseDC(hwnd, hdc); 7356 break; 7357 case WM_PAINT: 7358 if(win.buffer is null) 7359 goto default; 7360 7361 BITMAP bm; 7362 PAINTSTRUCT ps; 7363 7364 HDC hdc = BeginPaint(hwnd, &ps); 7365 7366 HDC hdcMem = CreateCompatibleDC(hdc); 7367 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7368 7369 GetObject(win.buffer, bm.sizeof, &bm); 7370 7371 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7372 7373 SelectObject(hdcMem, hbmOld); 7374 DeleteDC(hdcMem); 7375 EndPaint(hwnd, &ps); 7376 break; 7377 default: 7378 return DefWindowProc(hwnd, message, wparam, lparam); 7379 } 7380 7381 return 0; 7382 } 7383 7384 private wstring Win32Class(wstring name)() { 7385 static bool classRegistered; 7386 if(!classRegistered) { 7387 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7388 WNDCLASSEX wc; 7389 wc.cbSize = wc.sizeof; 7390 wc.hInstance = hInstance; 7391 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7392 wc.lpfnWndProc = &DoubleBufferWndProc; 7393 wc.lpszClassName = name.ptr; 7394 if(!RegisterClassExW(&wc)) 7395 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7396 classRegistered = true; 7397 } 7398 7399 return name; 7400 } 7401 7402 /+ 7403 version(win32_widgets) 7404 extern(Windows) 7405 private 7406 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7407 switch(iMessage) { 7408 case WM_PAINT: 7409 if(auto te = hWnd in Widget.nativeMapping) { 7410 try { 7411 //te.redraw(); 7412 writeln(te, " drawing"); 7413 } catch(Exception) {} 7414 } 7415 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7416 default: 7417 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7418 } 7419 } 7420 +/ 7421 7422 7423 /++ 7424 A widget specifically designed to hold other widgets. 7425 7426 History: 7427 Added July 1, 2021 7428 +/ 7429 class ContainerWidget : Widget { 7430 this(Widget parent) { 7431 super(parent); 7432 this.tabStop = false; 7433 7434 version(win32_widgets) { 7435 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7436 } 7437 } 7438 } 7439 7440 /++ 7441 A widget that takes your widget, puts scroll bars around it, and sends 7442 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7443 no effort to automatically scroll or clip its child widgets - it just sends 7444 the messages. 7445 7446 7447 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7448 The scroll coordinates are all given in a unit you interpret as you wish. One 7449 of these units is moved on each press of the arrow buttons and represents the 7450 smallest amount the user can scroll. The intention is for this to be one line, 7451 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7452 in each direction that the user might be interested in. 7453 7454 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7455 This is the amount it jumps when the user pressed page up and page down, or clicks 7456 in the exposed part of the scroll bar. 7457 7458 You should add child content to the ScrollMessageWidget. However, it is important to 7459 note that the coordinates are always independent of the scroll position! It is YOUR 7460 responsibility to do any necessary transforms, clipping, etc., while drawing the 7461 content and interpreting mouse events if they are supposed to change with the scroll. 7462 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7463 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7464 you more control (which can be considerably more efficient and adapted to your actual data) 7465 at the expense of you also needing to be aware of its reality. 7466 7467 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7468 version 10.3. Maybe this will change in the future.... but for now you must call 7469 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7470 +/ 7471 class ScrollMessageWidget : Widget { 7472 this(Widget parent) { 7473 super(parent); 7474 7475 container = new Widget(this); 7476 hsb = new HorizontalScrollbar(this); 7477 vsb = new VerticalScrollbar(this); 7478 7479 hsb.addEventListener("scrolltonextline", { 7480 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7481 notify(); 7482 }); 7483 hsb.addEventListener("scrolltopreviousline", { 7484 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7485 notify(); 7486 }); 7487 vsb.addEventListener("scrolltonextline", { 7488 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7489 notify(); 7490 }); 7491 vsb.addEventListener("scrolltopreviousline", { 7492 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7493 notify(); 7494 }); 7495 hsb.addEventListener("scrolltonextpage", { 7496 hsb.setPosition(hsb.position + hsb.step_); 7497 notify(); 7498 }); 7499 hsb.addEventListener("scrolltopreviouspage", { 7500 hsb.setPosition(hsb.position - hsb.step_); 7501 notify(); 7502 }); 7503 vsb.addEventListener("scrolltonextpage", { 7504 vsb.setPosition(vsb.position + vsb.step_); 7505 notify(); 7506 }); 7507 vsb.addEventListener("scrolltopreviouspage", { 7508 vsb.setPosition(vsb.position - vsb.step_); 7509 notify(); 7510 }); 7511 hsb.addEventListener("scrolltoposition", (Event event) { 7512 hsb.setPosition(event.intValue); 7513 notify(); 7514 }); 7515 vsb.addEventListener("scrolltoposition", (Event event) { 7516 vsb.setPosition(event.intValue); 7517 notify(); 7518 }); 7519 7520 7521 tabStop = false; 7522 container.tabStop = false; 7523 magic = true; 7524 } 7525 7526 private int movementPerButtonClickH_ = 1; 7527 private int movementPerButtonClickV_ = 1; 7528 public void movementPerButtonClick(int h, int v) { 7529 movementPerButtonClickH_ = h; 7530 movementPerButtonClickV_ = v; 7531 } 7532 7533 /++ 7534 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7535 7536 7537 The defaults for [addDefaultWheelListeners] are: 7538 7539 $(LIST 7540 * Mouse wheel scrolls vertically 7541 * Alt key + mouse wheel scrolls horiontally 7542 * Shift + mouse wheel scrolls faster. 7543 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7544 ) 7545 7546 The defaults for [addDefaultKeyboardListeners] are: 7547 7548 $(LIST 7549 * Arrow keys scroll by the given amounts 7550 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7551 * Page up and down scroll by the vertical viewable area 7552 * Home and end scroll to the start and end of the verticle viewable area. 7553 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7554 ) 7555 7556 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7557 7558 Params: 7559 horizontalArrowScrollAmount = 7560 verticalArrowScrollAmount = 7561 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7562 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7563 shiftMultiplier = multiplies the scroll amount by this when shift is held 7564 +/ 7565 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7566 auto _this = this; 7567 7568 container.addEventListener((scope KeyDownEvent ke) { 7569 switch(ke.key) { 7570 case Key.Left: 7571 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7572 break; 7573 case Key.Right: 7574 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7575 break; 7576 case Key.Up: 7577 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7578 break; 7579 case Key.Down: 7580 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7581 break; 7582 case Key.PageUp: 7583 if(ke.altKey) 7584 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7585 else 7586 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7587 break; 7588 case Key.PageDown: 7589 if(ke.altKey) 7590 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7591 else 7592 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7593 break; 7594 case Key.Home: 7595 if(ke.altKey) 7596 _this.scrollLeft(short.max * 16); 7597 else 7598 _this.scrollUp(short.max * 16); 7599 break; 7600 case Key.End: 7601 if(ke.altKey) 7602 _this.scrollRight(short.max * 16); 7603 else 7604 _this.scrollDown(short.max * 16); 7605 break; 7606 7607 default: 7608 // ignore, not for us. 7609 } 7610 7611 }); 7612 } 7613 7614 /// ditto 7615 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7616 auto _this = this; 7617 container.addEventListener((scope ClickEvent ce) { 7618 7619 if(ce.target && ce.target.tabStop) 7620 ce.target.focus(); 7621 7622 // ctrl is reserved for the application 7623 if(ce.ctrlKey) 7624 return; 7625 7626 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7627 return; 7628 7629 if(shiftMultiplier == 0 && ce.shiftKey) 7630 return; 7631 7632 if(ce.button == MouseButton.wheelDown) { 7633 if(ce.altKey) 7634 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7635 else 7636 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7637 } else if(ce.button == MouseButton.wheelUp) { 7638 if(ce.altKey) 7639 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7640 else 7641 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7642 } 7643 }); 7644 } 7645 7646 /++ 7647 Scrolls the given amount. 7648 7649 History: 7650 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. 7651 +/ 7652 void scrollUp(int amount = 1) { 7653 vsb.setPosition(vsb.position - amount); 7654 notify(); 7655 } 7656 /// ditto 7657 void scrollDown(int amount = 1) { 7658 vsb.setPosition(vsb.position + amount); 7659 notify(); 7660 } 7661 /// ditto 7662 void scrollLeft(int amount = 1) { 7663 hsb.setPosition(hsb.position - amount); 7664 notify(); 7665 } 7666 /// ditto 7667 void scrollRight(int amount = 1) { 7668 hsb.setPosition(hsb.position + amount); 7669 notify(); 7670 } 7671 7672 /// 7673 VerticalScrollbar verticalScrollBar() { return vsb; } 7674 /// 7675 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7676 7677 void notify() { 7678 static bool insideNotify; 7679 7680 if(insideNotify) 7681 return; // avoid the recursive call, even if it isn't strictly correct 7682 7683 insideNotify = true; 7684 scope(exit) insideNotify = false; 7685 7686 this.emit!ScrollEvent(); 7687 } 7688 7689 mixin Emits!ScrollEvent; 7690 7691 /// 7692 Point position() { 7693 return Point(hsb.position, vsb.position); 7694 } 7695 7696 /// 7697 void setPosition(int x, int y) { 7698 hsb.setPosition(x); 7699 vsb.setPosition(y); 7700 } 7701 7702 /// 7703 void setPageSize(int unitsX, int unitsY) { 7704 hsb.setStep(unitsX); 7705 vsb.setStep(unitsY); 7706 } 7707 7708 /// Always call this BEFORE setViewableArea 7709 void setTotalArea(int width, int height) { 7710 hsb.setMax(width); 7711 vsb.setMax(height); 7712 } 7713 7714 /++ 7715 Always set the viewable area AFTER setitng the total area if you are going to change both. 7716 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7717 If you need to do that, use [queueRecomputeChildLayout]. 7718 +/ 7719 void setViewableArea(int width, int height) { 7720 7721 // actually there IS A need to dothis cuz the max might have changed since then 7722 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7723 //return; // no need to do what is already done 7724 hsb.setViewableArea(width); 7725 vsb.setViewableArea(height); 7726 7727 bool needsNotify = false; 7728 7729 // FIXME: if at any point the rhs is outside the scrollbar, we need 7730 // to reset to 0. but it should remember the old position in case the 7731 // window resizes again, so it can kinda return ot where it was. 7732 // 7733 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7734 if(width >= hsb.max) { 7735 // there's plenty of room to display it all so we need to reset to zero 7736 // FIXME: adjust so it matches the note above 7737 hsb.setPosition(0); 7738 needsNotify = true; 7739 } 7740 if(height >= vsb.max) { 7741 // there's plenty of room to display it all so we need to reset to zero 7742 // FIXME: adjust so it matches the note above 7743 vsb.setPosition(0); 7744 needsNotify = true; 7745 } 7746 if(needsNotify) 7747 notify(); 7748 } 7749 7750 private bool magic; 7751 override void addChild(Widget w, int position = int.max) { 7752 if(magic) 7753 container.addChild(w, position); 7754 else 7755 super.addChild(w, position); 7756 } 7757 7758 override void recomputeChildLayout() { 7759 if(hsb is null || vsb is null || container is null) return; 7760 7761 registerMovement(); 7762 7763 enum BUTTON_SIZE = 16; 7764 7765 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7766 hsb.x = 0; 7767 hsb.y = this.height - hsb.height; 7768 7769 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7770 vsb.x = this.width - vsb.width; 7771 vsb.y = 0; 7772 7773 auto vsb_width = vsb.showing ? vsb.width : 0; 7774 auto hsb_height = hsb.showing ? hsb.height : 0; 7775 7776 hsb.width = this.width - vsb_width; 7777 vsb.height = this.height - hsb_height; 7778 7779 hsb.recomputeChildLayout(); 7780 vsb.recomputeChildLayout(); 7781 7782 if(this.header is null) { 7783 container.x = 0; 7784 container.y = 0; 7785 container.width = this.width - vsb_width; 7786 container.height = this.height - hsb_height; 7787 container.recomputeChildLayout(); 7788 } else { 7789 header.x = 0; 7790 header.y = 0; 7791 header.width = this.width - vsb_width; 7792 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7793 header.recomputeChildLayout(); 7794 7795 container.x = 0; 7796 container.y = scaleWithDpi(BUTTON_SIZE); 7797 container.width = this.width - vsb_width; 7798 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7799 container.recomputeChildLayout(); 7800 } 7801 } 7802 7803 private HorizontalScrollbar hsb; 7804 private VerticalScrollbar vsb; 7805 Widget container; 7806 private Widget header; 7807 7808 /++ 7809 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7810 7811 History: 7812 Added September 27, 2021 (dub v10.3) 7813 +/ 7814 Widget getHeader() { 7815 if(this.header is null) { 7816 magic = false; 7817 scope(exit) magic = true; 7818 this.header = new Widget(this); 7819 recomputeChildLayout(); 7820 } 7821 return this.header; 7822 } 7823 7824 /++ 7825 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7826 7827 History: 7828 Added January 3, 2023 (dub v11.0) 7829 +/ 7830 void scrollIntoView(Rectangle rect) { 7831 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7832 7833 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7834 7835 // the lower right is exclusive normally 7836 auto test = rect.lowerRight; 7837 if(test.x > 0) test.x--; 7838 if(test.y > 0) test.y--; 7839 7840 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7841 // try to scroll only one dimension at a time if we can 7842 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7843 setPosition(rect.upperLeft.x, position.y); 7844 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7845 setPosition(position.x, rect.upperLeft.y); 7846 } 7847 7848 } 7849 7850 override int minHeight() { 7851 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 7852 if(header !is null) 7853 min += header.minHeight; 7854 if(horizontalScrollBar.showing) 7855 min += horizontalScrollBar.minHeight; 7856 return min; 7857 } 7858 7859 override int maxHeight() { 7860 int max = container ? container.maxHeight : int.max; 7861 if(max == int.max) 7862 return max; 7863 if(horizontalScrollBar.showing) 7864 max += horizontalScrollBar.minHeight; 7865 return max; 7866 } 7867 } 7868 7869 /++ 7870 $(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") 7871 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7872 +/ 7873 version(minigui_screenshots) 7874 @Screenshot("ScrollMessageWidget") 7875 unittest { 7876 auto window = new Window("ScrollMessageWidget"); 7877 7878 auto smw = new ScrollMessageWidget(window); 7879 smw.addDefaultKeyboardListeners(); 7880 smw.addDefaultWheelListeners(); 7881 7882 window.loop(); 7883 } 7884 7885 /++ 7886 Bypasses automatic layout for its children, using manual positioning and sizing only. 7887 While you need to manually position them, you must ensure they are inside the StaticLayout's 7888 bounding box to avoid undefined behavior. 7889 7890 You should almost never use this. 7891 +/ 7892 class StaticLayout : Layout { 7893 /// 7894 this(Widget parent) { super(parent); } 7895 override void recomputeChildLayout() { 7896 registerMovement(); 7897 foreach(child; children) 7898 child.recomputeChildLayout(); 7899 } 7900 } 7901 7902 /++ 7903 Bypasses automatic positioning when being laid out. It is your responsibility to make 7904 room for this widget in the parent layout. 7905 7906 Its children are laid out normally, unless there is exactly one, in which case it takes 7907 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7908 can do that with `padding`). 7909 +/ 7910 class StaticPosition : Layout { 7911 /// 7912 this(Widget parent) { super(parent); } 7913 7914 override void recomputeChildLayout() { 7915 registerMovement(); 7916 if(this.children.length == 1) { 7917 auto child = children[0]; 7918 child.x = 0; 7919 child.y = 0; 7920 child.width = this.width; 7921 child.height = this.height; 7922 child.recomputeChildLayout(); 7923 } else 7924 foreach(child; children) 7925 child.recomputeChildLayout(); 7926 } 7927 7928 alias width = typeof(super).width; 7929 alias height = typeof(super).height; 7930 7931 @property int width(int w) @nogc pure @safe nothrow { 7932 return this._width = w; 7933 } 7934 7935 @property int height(int w) @nogc pure @safe nothrow { 7936 return this._height = w; 7937 } 7938 7939 } 7940 7941 /++ 7942 FixedPosition is like [StaticPosition], but its coordinates 7943 are always relative to the viewport, meaning they do not scroll with 7944 the parent content. 7945 +/ 7946 class FixedPosition : StaticPosition { 7947 /// 7948 this(Widget parent) { super(parent); } 7949 } 7950 7951 version(win32_widgets) 7952 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7953 if(true) { 7954 // cmd == 0 = menu, cmd == 1 = accelerator 7955 if(auto item = idm in Action.mapping) { 7956 foreach(handler; (*item).triggered) 7957 handler(); 7958 /* 7959 auto event = new Event("triggered", *item); 7960 event.button = idm; 7961 event.dispatch(); 7962 */ 7963 return 0; 7964 } 7965 } 7966 if(handle) 7967 if(auto widgetp = handle in Widget.nativeMapping) { 7968 (*widgetp).handleWmCommand(cmd, idm); 7969 return 0; 7970 } 7971 return 1; 7972 } 7973 7974 7975 /// 7976 class Window : Widget { 7977 int mouseCaptureCount = 0; 7978 Widget mouseCapturedBy; 7979 void captureMouse(Widget byWhom) { 7980 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7981 mouseCaptureCount++; 7982 mouseCapturedBy = byWhom; 7983 win.grabInput(); 7984 } 7985 void releaseMouseCapture() { 7986 mouseCaptureCount--; 7987 mouseCapturedBy = null; 7988 win.releaseInputGrab(); 7989 } 7990 7991 /++ 7992 Sets the window icon which is often seen in title bars and taskbars. 7993 7994 History: 7995 Added April 5, 2022 (dub v10.8) 7996 +/ 7997 @property void icon(MemoryImage icon) { 7998 if(win && icon) 7999 win.icon = icon; 8000 } 8001 8002 /// 8003 @scriptable 8004 @property bool focused() { 8005 return win.focused; 8006 } 8007 8008 static class Style : Widget.Style { 8009 override WidgetBackground background() { 8010 version(custom_widgets) 8011 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8012 else version(win32_widgets) 8013 return WidgetBackground(Color.transparent); 8014 else static assert(0); 8015 } 8016 } 8017 mixin OverrideStyle!Style; 8018 8019 /++ 8020 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. 8021 +/ 8022 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8023 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8024 } 8025 8026 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8027 OperatingSystemFont font; 8028 if(auto vt = WidgetPainter.visualTheme) { 8029 font = vt.defaultFontCached(96); // FIXME 8030 } 8031 8032 if(font is null) { 8033 static int defaultHeightCache; 8034 if(defaultHeightCache == 0) { 8035 font = new OperatingSystemFont; 8036 font.loadDefault; 8037 defaultHeightCache = font.height();// * 5 / 4; 8038 } 8039 return defaultHeightCache; 8040 } 8041 8042 return font.height();// * 5 / 4; 8043 } 8044 8045 Widget focusedWidget; 8046 8047 private SimpleWindow win_; 8048 8049 @property { 8050 /++ 8051 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8052 8053 History: 8054 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8055 +/ 8056 public SimpleWindow win() { 8057 return win_; 8058 } 8059 /// 8060 protected void win(SimpleWindow w) { 8061 win_ = w; 8062 } 8063 } 8064 8065 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8066 this(Widget p) { 8067 tabStop = false; 8068 super(p); 8069 } 8070 8071 private void actualRedraw() { 8072 if(recomputeChildLayoutRequired) 8073 recomputeChildLayoutEntry(); 8074 if(!showing) return; 8075 8076 assert(parentWindow !is null); 8077 8078 auto w = drawableWindow; 8079 if(w is null) 8080 w = parentWindow.win; 8081 8082 if(w.closed()) 8083 return; 8084 8085 auto ugh = this.parent; 8086 int lox, loy; 8087 while(ugh) { 8088 lox += ugh.x; 8089 loy += ugh.y; 8090 ugh = ugh.parent; 8091 } 8092 auto painter = w.draw(true); 8093 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8094 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 8095 } 8096 8097 8098 private bool skipNextChar = false; 8099 8100 /++ 8101 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8102 8103 This constructor is intended primarily for internal use and may be changed to `protected` later. 8104 +/ 8105 this(SimpleWindow win) { 8106 8107 static if(UsingSimpledisplayX11) { 8108 win.discardAdditionalConnectionState = &discardXConnectionState; 8109 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8110 } 8111 8112 tabStop = false; 8113 super(null); 8114 this.win = win; 8115 8116 win.addEventListener((Widget.RedrawEvent) { 8117 if(win.eventQueued!RecomputeEvent) { 8118 // writeln("skipping"); 8119 return; // let the recompute event do the actual redraw 8120 } 8121 this.actualRedraw(); 8122 }); 8123 8124 win.addEventListener((Widget.RecomputeEvent) { 8125 recomputeChildLayoutEntry(); 8126 if(win.eventQueued!RedrawEvent) 8127 return; // let the queued one do it 8128 else { 8129 // writeln("drawing"); 8130 this.actualRedraw(); // if not queued, it needs to be done now anyway 8131 } 8132 }); 8133 8134 this.width = win.width; 8135 this.height = win.height; 8136 this.parentWindow = this; 8137 8138 win.closeQuery = () { 8139 if(this.emit!ClosingEvent()) 8140 win.close(); 8141 }; 8142 win.onClosing = () { 8143 this.emit!ClosedEvent(); 8144 }; 8145 8146 win.windowResized = (int w, int h) { 8147 this.width = w; 8148 this.height = h; 8149 recomputeChildLayout(); 8150 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8151 //version(win32_widgets) 8152 //InvalidateRect(hwnd, null, true); 8153 redraw(); 8154 }; 8155 8156 win.onFocusChange = (bool getting) { 8157 if(this.focusedWidget) { 8158 if(getting) { 8159 this.focusedWidget.emit!FocusEvent(); 8160 this.focusedWidget.emit!FocusInEvent(); 8161 } else { 8162 this.focusedWidget.emit!BlurEvent(); 8163 this.focusedWidget.emit!FocusOutEvent(); 8164 } 8165 } 8166 8167 if(getting) { 8168 this.emit!FocusEvent(); 8169 this.emit!FocusInEvent(); 8170 } else { 8171 this.emit!BlurEvent(); 8172 this.emit!FocusOutEvent(); 8173 } 8174 }; 8175 8176 win.onDpiChanged = { 8177 this.queueRecomputeChildLayout(); 8178 auto event = new DpiChangedEvent(this); 8179 event.sendDirectly(); 8180 8181 privateDpiChanged(); 8182 }; 8183 8184 win.setEventHandlers( 8185 (MouseEvent e) { 8186 dispatchMouseEvent(e); 8187 }, 8188 (KeyEvent e) { 8189 //writefln("%x %s", cast(uint) e.key, e.key); 8190 dispatchKeyEvent(e); 8191 }, 8192 (dchar e) { 8193 if(e == 13) e = 10; // hack? 8194 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8195 dispatchCharEvent(e); 8196 }, 8197 ); 8198 8199 addEventListener("char", (Widget, Event ev) { 8200 if(skipNextChar) { 8201 ev.preventDefault(); 8202 skipNextChar = false; 8203 } 8204 }); 8205 8206 version(win32_widgets) 8207 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8208 if(hwnd !is this.win.impl.hwnd) 8209 return 1; // we don't care... pass it on 8210 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8211 if(mustReturn) 8212 return ret; 8213 return 1; // pass it on 8214 }; 8215 8216 if(Window.newWindowCreated) 8217 Window.newWindowCreated(this); 8218 } 8219 8220 version(custom_widgets) 8221 override void defaultEventHandler_click(ClickEvent event) { 8222 if(event.target && event.target.tabStop) 8223 event.target.focus(); 8224 } 8225 8226 private static void delegate(Window) newWindowCreated; 8227 8228 version(win32_widgets) 8229 override void paint(WidgetPainter painter) { 8230 /* 8231 RECT rect; 8232 rect.right = this.width; 8233 rect.bottom = this.height; 8234 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8235 */ 8236 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8237 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8238 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8239 // since the pen is null, to fill the whole space, we need the +1 on both. 8240 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8241 SelectObject(painter.impl.hdc, p); 8242 SelectObject(painter.impl.hdc, b); 8243 } 8244 version(custom_widgets) 8245 override void paint(WidgetPainter painter) { 8246 auto cs = getComputedStyle(); 8247 painter.fillColor = cs.windowBackgroundColor; 8248 painter.outlineColor = cs.windowBackgroundColor; 8249 painter.drawRectangle(Point(0, 0), this.width, this.height); 8250 } 8251 8252 8253 override void defaultEventHandler_keydown(KeyDownEvent event) { 8254 Widget _this = event.target; 8255 8256 if(event.key == Key.Tab) { 8257 /* Window tab ordering is a recursive thingy with each group */ 8258 8259 // FIXME inefficient 8260 Widget[] helper(Widget p) { 8261 if(p.hidden) 8262 return null; 8263 Widget[] childOrdering; 8264 8265 auto children = p.children.dup; 8266 8267 while(true) { 8268 // UIs should be generally small, so gonna brute force it a little 8269 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8270 8271 Widget smallestTab; 8272 foreach(ref c; children) { 8273 if(c is null) continue; 8274 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8275 smallestTab = c; 8276 c = null; 8277 } 8278 } 8279 if(smallestTab !is null) { 8280 if(smallestTab.tabStop && !smallestTab.hidden) 8281 childOrdering ~= smallestTab; 8282 if(!smallestTab.hidden) 8283 childOrdering ~= helper(smallestTab); 8284 } else 8285 break; 8286 8287 } 8288 8289 return childOrdering; 8290 } 8291 8292 Widget[] tabOrdering = helper(this); 8293 8294 Widget recipient; 8295 8296 if(tabOrdering.length) { 8297 bool seenThis = false; 8298 Widget previous; 8299 foreach(idx, child; tabOrdering) { 8300 if(child is focusedWidget) { 8301 8302 if(event.shiftKey) { 8303 if(idx == 0) 8304 recipient = tabOrdering[$-1]; 8305 else 8306 recipient = tabOrdering[idx - 1]; 8307 break; 8308 } 8309 8310 seenThis = true; 8311 if(idx + 1 == tabOrdering.length) { 8312 // we're at the end, either move to the next group 8313 // or start back over 8314 recipient = tabOrdering[0]; 8315 } 8316 continue; 8317 } 8318 if(seenThis) { 8319 recipient = child; 8320 break; 8321 } 8322 previous = child; 8323 } 8324 } 8325 8326 if(recipient !is null) { 8327 // writeln(typeid(recipient)); 8328 recipient.focus(); 8329 8330 skipNextChar = true; 8331 } 8332 } 8333 8334 debug if(event.key == Key.F12) { 8335 if(devTools) { 8336 devTools.close(); 8337 devTools = null; 8338 } else { 8339 devTools = new DevToolWindow(this); 8340 devTools.show(); 8341 } 8342 } 8343 } 8344 8345 debug DevToolWindow devTools; 8346 8347 8348 /++ 8349 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8350 8351 History: 8352 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8353 8354 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8355 +/ 8356 this(int width = 500, int height = 500, string title = null) { 8357 if(title is null) { 8358 import core.runtime; 8359 if(Runtime.args.length) 8360 title = Runtime.args[0]; 8361 } 8362 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8363 8364 static if(UsingSimpledisplayX11) { 8365 ///+ 8366 // for input proxy 8367 auto display = XDisplayConnection.get; 8368 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8369 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8370 XMapWindow(display, inputProxy); 8371 // writefln("input proxy: 0x%0x", inputProxy); 8372 this.inputProxy = new SimpleWindow(inputProxy); 8373 8374 XEvent lastEvent; 8375 this.inputProxy.handleNativeEvent = (XEvent ev) { 8376 lastEvent = ev; 8377 return 1; 8378 }; 8379 this.inputProxy.setEventHandlers( 8380 (MouseEvent e) { 8381 dispatchMouseEvent(e); 8382 }, 8383 (KeyEvent e) { 8384 //writefln("%x %s", cast(uint) e.key, e.key); 8385 if(dispatchKeyEvent(e)) { 8386 // FIXME: i should trap error 8387 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8388 auto thing = nw.focusableWindow(); 8389 if(thing && thing.window) { 8390 lastEvent.xkey.window = thing.window; 8391 // writeln("sending event ", lastEvent.xkey); 8392 trapXErrors( { 8393 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8394 }); 8395 } 8396 } 8397 } 8398 }, 8399 (dchar e) { 8400 if(e == 13) e = 10; // hack? 8401 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8402 dispatchCharEvent(e); 8403 }, 8404 ); 8405 8406 this.inputProxy.populateXic(); 8407 // done 8408 //+/ 8409 } 8410 8411 8412 8413 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8414 8415 this(win); 8416 } 8417 8418 SimpleWindow inputProxy; 8419 8420 private SimpleWindow setRequestedInputFocus() { 8421 return inputProxy; 8422 } 8423 8424 /// ditto 8425 this(string title, int width = 500, int height = 500) { 8426 this(width, height, title); 8427 } 8428 8429 /// 8430 @property string title() { return parentWindow.win.title; } 8431 /// 8432 @property void title(string title) { parentWindow.win.title = title; } 8433 8434 /// 8435 @scriptable 8436 void close() { 8437 win.close(); 8438 // I synchronize here upon window closing to ensure all child windows 8439 // get updated too before the event loop. This avoids some random X errors. 8440 static if(UsingSimpledisplayX11) { 8441 runInGuiThread( { 8442 XSync(XDisplayConnection.get, false); 8443 }); 8444 } 8445 } 8446 8447 bool dispatchKeyEvent(KeyEvent ev) { 8448 auto wid = focusedWidget; 8449 if(wid is null) 8450 wid = this; 8451 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8452 event.originalKeyEvent = ev; 8453 event.key = ev.key; 8454 event.state = ev.modifierState; 8455 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8456 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8457 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8458 event.dispatch(); 8459 8460 return !event.propagationStopped; 8461 } 8462 8463 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8464 bool dispatchCharEvent(dchar ch) { 8465 if(focusedWidget) { 8466 auto event = new CharEvent(focusedWidget, ch); 8467 event.dispatch(); 8468 return !event.propagationStopped; 8469 } 8470 return true; 8471 } 8472 8473 Widget mouseLastOver; 8474 Widget mouseLastDownOn; 8475 bool lastWasDoubleClick; 8476 bool dispatchMouseEvent(MouseEvent ev) { 8477 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8478 auto ele = eleR.widget; 8479 8480 auto captureEle = ele; 8481 8482 if(mouseCapturedBy !is null) { 8483 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8484 captureEle = mouseCapturedBy; 8485 } 8486 8487 // a hack to get it relative to the widget. 8488 eleR.x = ev.x; 8489 eleR.y = ev.y; 8490 auto pain = captureEle; 8491 while(pain) { 8492 eleR.x -= pain.x; 8493 eleR.y -= pain.y; 8494 pain.addScrollPosition(eleR.x, eleR.y); 8495 pain = pain.parent; 8496 } 8497 8498 void populateMouseEventBase(MouseEventBase event) { 8499 event.button = ev.button; 8500 event.buttonLinear = ev.buttonLinear; 8501 event.state = ev.modifierState; 8502 event.clientX = eleR.x; 8503 event.clientY = eleR.y; 8504 8505 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8506 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8507 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8508 } 8509 8510 if(ev.type == MouseEventType.buttonPressed) { 8511 { 8512 auto event = new MouseDownEvent(captureEle); 8513 populateMouseEventBase(event); 8514 event.dispatch(); 8515 } 8516 8517 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8518 auto event = new DoubleClickEvent(captureEle); 8519 populateMouseEventBase(event); 8520 event.dispatch(); 8521 lastWasDoubleClick = ev.doubleClick; 8522 } else { 8523 lastWasDoubleClick = false; 8524 } 8525 8526 mouseLastDownOn = ele; 8527 } else if(ev.type == MouseEventType.buttonReleased) { 8528 { 8529 auto event = new MouseUpEvent(captureEle); 8530 populateMouseEventBase(event); 8531 event.dispatch(); 8532 } 8533 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8534 auto event = new ClickEvent(captureEle); 8535 populateMouseEventBase(event); 8536 event.dispatch(); 8537 } 8538 } else if(ev.type == MouseEventType.motion) { 8539 // motion 8540 { 8541 auto event = new MouseMoveEvent(captureEle); 8542 populateMouseEventBase(event); // fills in button which is meaningless but meh 8543 event.dispatch(); 8544 } 8545 8546 if(mouseLastOver !is ele) { 8547 if(ele !is null) { 8548 if(!isAParentOf(ele, mouseLastOver)) { 8549 ele.setDynamicState(DynamicState.hover, true); 8550 auto event = new MouseEnterEvent(ele); 8551 event.relatedTarget = mouseLastOver; 8552 event.sendDirectly(); 8553 8554 ele.useStyleProperties((scope Widget.Style s) { 8555 ele.parentWindow.win.cursor = s.cursor; 8556 }); 8557 } 8558 } 8559 8560 if(mouseLastOver !is null) { 8561 if(!isAParentOf(mouseLastOver, ele)) { 8562 mouseLastOver.setDynamicState(DynamicState.hover, false); 8563 auto event = new MouseLeaveEvent(mouseLastOver); 8564 event.relatedTarget = ele; 8565 event.sendDirectly(); 8566 } 8567 } 8568 8569 if(ele !is null) { 8570 auto event = new MouseOverEvent(ele); 8571 event.relatedTarget = mouseLastOver; 8572 event.dispatch(); 8573 } 8574 8575 if(mouseLastOver !is null) { 8576 auto event = new MouseOutEvent(mouseLastOver); 8577 event.relatedTarget = ele; 8578 event.dispatch(); 8579 } 8580 8581 mouseLastOver = ele; 8582 } 8583 } 8584 8585 return true; // FIXME: the event default prevented? 8586 } 8587 8588 /++ 8589 Shows the window and runs the application event loop. 8590 8591 Blocks until this window is closed. 8592 8593 Bugs: 8594 8595 $(PITFALL 8596 You should always have one event loop live for your application. 8597 If you make two windows in sequence, the second call to loop (or 8598 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8599 might fail: 8600 8601 --- 8602 // don't do this! 8603 auto window = new Window(); 8604 window.loop(); 8605 8606 // or new Window or new MainWindow, all the same 8607 auto window2 = new SimpleWindow(); 8608 window2.eventLoop(0); // problematic! might crash 8609 --- 8610 8611 simpledisplay's current implementation assumes that final cleanup is 8612 done when the event loop refcount reaches zero. So after the first 8613 eventLoop returns, when there isn't already another one active, it assumes 8614 the program will exit soon and cleans up. 8615 8616 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8617 it eventually, but in the mean time, there's an easy solution: 8618 8619 --- 8620 // do this 8621 EventLoop mainEventLoop = EventLoop.get; // just add this line 8622 8623 auto window = new Window(); 8624 window.loop(); 8625 8626 // or any other type of Window etc. 8627 auto window2 = new Window(); 8628 window2.loop(); // perfectly fine since mainEventLoop still alive 8629 --- 8630 8631 By adding a top-level reference to the event loop, it ensures the final cleanup 8632 is not performed until it goes out of scope too, letting the individual window loops 8633 work without trouble despite the bug. 8634 ) 8635 8636 History: 8637 The [BlockingMode] parameter was added on December 8, 2021. 8638 The default behavior is to block until the application quits 8639 (so all windows have been closed), unless another minigui or 8640 simpledisplay event loop is already running, in which case it 8641 will block until this window closes specifically. 8642 +/ 8643 @scriptable 8644 void loop(BlockingMode bm = BlockingMode.automatic) { 8645 if(win.closed) 8646 return; // otherwise show will throw 8647 show(); 8648 win.eventLoopWithBlockingMode(bm, 0); 8649 } 8650 8651 private bool firstShow = true; 8652 8653 @scriptable 8654 override void show() { 8655 bool rd = false; 8656 if(firstShow) { 8657 firstShow = false; 8658 recomputeChildLayout(); 8659 auto f = getFirstFocusable(this); // FIXME: autofocus? 8660 if(f) 8661 f.focus(); 8662 redraw(); 8663 } 8664 win.show(); 8665 super.show(); 8666 } 8667 @scriptable 8668 override void hide() { 8669 win.hide(); 8670 super.hide(); 8671 } 8672 8673 static Widget getFirstFocusable(Widget start) { 8674 if(start is null) 8675 return null; 8676 8677 foreach(widget; &start.focusableWidgets) { 8678 return widget; 8679 } 8680 8681 return null; 8682 } 8683 8684 static Widget getLastFocusable(Widget start) { 8685 if(start is null) 8686 return null; 8687 8688 Widget last; 8689 foreach(widget; &start.focusableWidgets) { 8690 last = widget; 8691 } 8692 8693 return last; 8694 } 8695 8696 8697 mixin Emits!ClosingEvent; 8698 mixin Emits!ClosedEvent; 8699 } 8700 8701 /++ 8702 History: 8703 Added January 12, 2022 8704 +/ 8705 class DpiChangedEvent : Event { 8706 enum EventString = "dpichanged"; 8707 8708 this(Widget target) { 8709 super(EventString, target); 8710 } 8711 } 8712 8713 debug private class DevToolWindow : Window { 8714 Window p; 8715 8716 TextEdit parentList; 8717 TextEdit logWindow; 8718 TextLabel clickX, clickY; 8719 8720 this(Window p) { 8721 this.p = p; 8722 super(400, 300, "Developer Toolbox"); 8723 8724 logWindow = new TextEdit(this); 8725 parentList = new TextEdit(this); 8726 8727 auto hl = new HorizontalLayout(this); 8728 clickX = new TextLabel("", TextAlignment.Right, hl); 8729 clickY = new TextLabel("", TextAlignment.Right, hl); 8730 8731 parentListeners ~= p.addEventListener("*", (Event ev) { 8732 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8733 }); 8734 8735 parentListeners ~= p.addEventListener((ClickEvent ev) { 8736 auto s = ev.srcElement; 8737 8738 string list; 8739 8740 void addInfo(Widget s) { 8741 list ~= s.toString(); 8742 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8743 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8744 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8745 list ~= "\n\theight: " ~ toInternal!string(s.height); 8746 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 8747 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 8748 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 8749 list ~= "\n\twidth: " ~ toInternal!string(s.width); 8750 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8751 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8752 } 8753 8754 addInfo(s); 8755 8756 s = s.parent; 8757 while(s) { 8758 list ~= "\n"; 8759 addInfo(s); 8760 s = s.parent; 8761 } 8762 parentList.content = list; 8763 8764 clickX.label = toInternal!string(ev.clientX); 8765 clickY.label = toInternal!string(ev.clientY); 8766 }); 8767 } 8768 8769 EventListener[] parentListeners; 8770 8771 override void close() { 8772 assert(p !is null); 8773 foreach(p; parentListeners) 8774 p.disconnect(); 8775 parentListeners = null; 8776 p.devTools = null; 8777 p = null; 8778 super.close(); 8779 } 8780 8781 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8782 if(ev.key == Key.F12) { 8783 this.close(); 8784 if(p) 8785 p.devTools = null; 8786 } else { 8787 super.defaultEventHandler_keydown(ev); 8788 } 8789 } 8790 8791 void log(T...)(T t) { 8792 string str; 8793 import std.conv; 8794 foreach(i; t) 8795 str ~= to!string(i); 8796 str ~= "\n"; 8797 logWindow.addText(str); 8798 8799 //version(custom_widgets) 8800 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8801 } 8802 } 8803 8804 /++ 8805 A dialog is a transient window that intends to get information from 8806 the user before being dismissed. 8807 +/ 8808 abstract class Dialog : Window { 8809 /// 8810 this(int width, int height, string title = null) { 8811 super(width, height, title); 8812 } 8813 8814 /// 8815 abstract void OK(); 8816 8817 /// 8818 void Cancel() { 8819 this.close(); 8820 } 8821 } 8822 8823 /++ 8824 A custom widget similar to the HTML5 <details> tag. 8825 +/ 8826 version(none) 8827 class DetailsView : Widget { 8828 8829 } 8830 8831 // FIXME: maybe i should expose the other list views Windows offers too 8832 8833 /++ 8834 A TableView is a widget made to display a table of data strings. 8835 8836 8837 Future_Directions: 8838 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8839 8840 I will add a selection changed event at some point, as well as item clicked events. 8841 History: 8842 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8843 See_Also: 8844 [ListWidget] which displays a list of strings without additional columns. 8845 +/ 8846 class TableView : Widget { 8847 /++ 8848 8849 +/ 8850 this(Widget parent) { 8851 super(parent); 8852 8853 version(win32_widgets) { 8854 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8855 } else version(custom_widgets) { 8856 auto smw = new ScrollMessageWidget(this); 8857 smw.addDefaultKeyboardListeners(); 8858 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8859 tvwi = new TableViewWidgetInner(this, smw); 8860 } 8861 } 8862 8863 // FIXME: auto-size columns on double click of header thing like in Windows 8864 // it need only make the currently displayed things fit well. 8865 8866 8867 private ColumnInfo[] columns; 8868 private int itemCount; 8869 8870 version(custom_widgets) private { 8871 TableViewWidgetInner tvwi; 8872 } 8873 8874 /// Passed to [setColumnInfo] 8875 static struct ColumnInfo { 8876 const(char)[] name; /// the name displayed in the header 8877 /++ 8878 The default width, in pixels. As a special case, you can set this to -1 8879 if you want the system to try to automatically size the width to fit visible 8880 content. If it can't, it will try to pick a sensible default size. 8881 8882 Any other negative value is not allowed and may lead to unpredictable results. 8883 8884 History: 8885 The -1 behavior was specified on December 3, 2021. It actually worked before 8886 anyway on Win32 but now it is a formal feature with partial Linux support. 8887 8888 Bugs: 8889 It doesn't actually attempt to calculate a best-fit width on Linux as of 8890 December 3, 2021. I do plan to fix this in the future, but Windows is the 8891 priority right now. At least it doesn't break things when you use it now. 8892 +/ 8893 int width; 8894 8895 /++ 8896 Alignment of the text in the cell. Applies to the header as well as all data in this 8897 column. 8898 8899 Bugs: 8900 On Windows, the first column ignores this member and is always left aligned. 8901 You can work around this by inserting a dummy first column with width = 0 8902 then putting your actual data in the second column, which does respect the 8903 alignment. 8904 8905 This is a quirk of the operating system's implementation going back a very 8906 long time and is unlikely to ever be fixed. 8907 +/ 8908 TextAlignment alignment; 8909 8910 /++ 8911 After all the pixel widths have been assigned, any left over 8912 space is divided up among all columns and distributed to according 8913 to the widthPercent field. 8914 8915 8916 For example, if you have two fields, both with width 50 and one with 8917 widthPercent of 25 and the other with widthPercent of 75, and the 8918 container is 200 pixels wide, first both get their width of 50. 8919 then the 100 remaining pixels are split up, so the one gets a total 8920 of 75 pixels and the other gets a total of 125. 8921 8922 This is automatically applied as the window is resized. 8923 8924 If there is not enough space - that is, when a horizontal scrollbar 8925 needs to appear - there are 0 pixels divided up, and thus everyone 8926 gets 0. This can cause a column to shrink out of proportion when 8927 passing the scroll threshold. 8928 8929 It is important to still set a fixed width (that is, to populate the 8930 `width` field) even if you use the percents because that will be the 8931 default minimum in the event of a scroll bar appearing. 8932 8933 The percents total in the column can never exceed 100 or be less than 0. 8934 Doing this will trigger an assert error. 8935 8936 Implementation note: 8937 8938 Please note that percentages are only recalculated 1) upon original 8939 construction and 2) upon resizing the control. If the user adjusts the 8940 width of a column, the percentage items will not be updated. 8941 8942 On the other hand, if the user adjusts the width of a percentage column 8943 then resizes the window, it is recalculated, meaning their hand adjustment 8944 is discarded. This specific behavior may change in the future as it is 8945 arguably a bug, but I'm not certain yet. 8946 8947 History: 8948 Added November 10, 2021 (dub v10.4) 8949 +/ 8950 int widthPercent; 8951 8952 8953 private int calculatedWidth; 8954 } 8955 /++ 8956 Sets the number of columns along with information about the headers. 8957 8958 Please note: on Windows, the first column ignores your alignment preference 8959 and is always left aligned. 8960 +/ 8961 void setColumnInfo(ColumnInfo[] columns...) { 8962 8963 foreach(ref c; columns) { 8964 c.name = c.name.idup; 8965 } 8966 this.columns = columns.dup; 8967 8968 updateCalculatedWidth(false); 8969 8970 version(custom_widgets) { 8971 tvwi.header.updateHeaders(); 8972 tvwi.updateScrolls(); 8973 } else version(win32_widgets) 8974 foreach(i, column; this.columns) { 8975 LVCOLUMN lvColumn; 8976 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8977 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8978 8979 auto bfr = WCharzBuffer(column.name); 8980 lvColumn.pszText = bfr.ptr; 8981 8982 if(column.alignment & TextAlignment.Center) 8983 lvColumn.fmt = LVCFMT_CENTER; 8984 else if(column.alignment & TextAlignment.Right) 8985 lvColumn.fmt = LVCFMT_RIGHT; 8986 else 8987 lvColumn.fmt = LVCFMT_LEFT; 8988 8989 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8990 throw new WindowsApiException("Insert Column Fail", GetLastError()); 8991 } 8992 } 8993 8994 private int getActualSetSize(size_t i, bool askWindows) { 8995 version(win32_widgets) 8996 if(askWindows) 8997 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8998 auto w = columns[i].width; 8999 if(w == -1) 9000 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 9001 return w; 9002 } 9003 9004 private void updateCalculatedWidth(bool informWindows) { 9005 int padding; 9006 version(win32_widgets) 9007 padding = 4; 9008 int remaining = this.width; 9009 foreach(i, column; columns) 9010 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9011 remaining -= padding; 9012 if(remaining < 0) 9013 remaining = 0; 9014 9015 int percentTotal; 9016 foreach(i, ref column; columns) { 9017 percentTotal += column.widthPercent; 9018 9019 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9020 9021 column.calculatedWidth = c; 9022 9023 version(win32_widgets) 9024 if(informWindows) 9025 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9026 } 9027 9028 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9029 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)."); 9030 9031 9032 } 9033 9034 override void registerMovement() { 9035 super.registerMovement(); 9036 9037 updateCalculatedWidth(true); 9038 } 9039 9040 /++ 9041 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. 9042 +/ 9043 void setItemCount(int count) { 9044 this.itemCount = count; 9045 version(custom_widgets) { 9046 tvwi.updateScrolls(); 9047 redraw(); 9048 } else version(win32_widgets) { 9049 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9050 } 9051 } 9052 9053 /++ 9054 Clears all items; 9055 +/ 9056 void clear() { 9057 this.itemCount = 0; 9058 this.columns = null; 9059 version(custom_widgets) { 9060 tvwi.header.updateHeaders(); 9061 tvwi.updateScrolls(); 9062 redraw(); 9063 } else version(win32_widgets) { 9064 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9065 } 9066 } 9067 9068 /+ 9069 version(win32_widgets) 9070 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9071 auto itemId = dis.itemID; 9072 auto hdc = dis.hDC; 9073 auto rect = dis.rcItem; 9074 switch(dis.itemAction) { 9075 case ODA_DRAWENTIRE: 9076 9077 // FIXME: do other items 9078 // FIXME: do the focus rectangle i guess 9079 // FIXME: alignment 9080 // FIXME: column width 9081 // FIXME: padding left 9082 // FIXME: check dpi scaling 9083 // FIXME: don't owner draw unless it is necessary. 9084 9085 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9086 RECT itemRect; 9087 itemRect.top = 1; // subitem idx, 1-based 9088 itemRect.left = LVIR_BOUNDS; 9089 9090 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9091 itemRect.left += padding; 9092 9093 getData(itemId, 0, (in char[] data) { 9094 auto wdata = WCharzBuffer(data); 9095 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9096 9097 }); 9098 goto case; 9099 case ODA_FOCUS: 9100 if(dis.itemState & ODS_FOCUS) 9101 DrawFocusRect(hdc, &rect); 9102 break; 9103 case ODA_SELECT: 9104 // itemState & ODS_SELECTED 9105 break; 9106 default: 9107 } 9108 return 1; 9109 } 9110 +/ 9111 9112 version(win32_widgets) { 9113 CellStyle last; 9114 COLORREF defaultColor; 9115 COLORREF defaultBackground; 9116 } 9117 9118 version(win32_widgets) 9119 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9120 switch(code) { 9121 case NM_CUSTOMDRAW: 9122 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9123 switch(s.nmcd.dwDrawStage) { 9124 case CDDS_PREPAINT: 9125 if(getCellStyle is null) 9126 return 0; 9127 9128 mustReturn = true; 9129 return CDRF_NOTIFYITEMDRAW; 9130 case CDDS_ITEMPREPAINT: 9131 mustReturn = true; 9132 return CDRF_NOTIFYSUBITEMDRAW; 9133 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9134 mustReturn = true; 9135 9136 if(getCellStyle is null) // this SHOULD never happen... 9137 return 0; 9138 9139 if(s.iSubItem == 0) { 9140 // Windows resets it per row so we'll use item 0 as a chance 9141 // to capture these for later 9142 defaultColor = s.clrText; 9143 defaultBackground = s.clrTextBk; 9144 } 9145 9146 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9147 // if no special style and no reset needed... 9148 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9149 return 0; // allow default processing to continue 9150 9151 last = style; 9152 9153 // might still need to reset or use the preference. 9154 9155 if(style.flags & CellStyle.Flags.textColorSet) 9156 s.clrText = style.textColor.asWindowsColorRef; 9157 else 9158 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9159 if(style.flags & CellStyle.Flags.backgroundColorSet) 9160 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9161 else 9162 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9163 9164 return CDRF_NEWFONT; 9165 default: 9166 return 0; 9167 9168 } 9169 case NM_RETURN: // no need since i subclass keydown 9170 break; 9171 case LVN_COLUMNCLICK: 9172 auto info = cast(LPNMLISTVIEW) hdr; 9173 this.emit!HeaderClickedEvent(info.iSubItem); 9174 break; 9175 case NM_CLICK: 9176 case NM_DBLCLK: 9177 case NM_RCLICK: 9178 case NM_RDBLCLK: 9179 // the item/subitem is set here and that can be a useful notification 9180 // even beyond the normal click notification 9181 break; 9182 case LVN_GETDISPINFO: 9183 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9184 if(info.item.mask & LVIF_TEXT) { 9185 if(getData) { 9186 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9187 auto bfr = WCharzBuffer(dataReceived); 9188 auto len = info.item.cchTextMax; 9189 if(bfr.length < len) 9190 len = cast(typeof(len)) bfr.length; 9191 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9192 info.item.pszText[len] = 0; 9193 }); 9194 } else { 9195 info.item.pszText[0] = 0; 9196 } 9197 //info.item.iItem 9198 //if(info.item.iSubItem) 9199 } 9200 break; 9201 default: 9202 } 9203 return 0; 9204 } 9205 9206 override bool encapsulatedChildren() { 9207 return true; 9208 } 9209 9210 /++ 9211 Informs the control that content has changed. 9212 9213 History: 9214 Added November 10, 2021 (dub v10.4) 9215 +/ 9216 void update() { 9217 version(custom_widgets) 9218 redraw(); 9219 else { 9220 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9221 UpdateWindow(hwnd); 9222 } 9223 9224 9225 } 9226 9227 /++ 9228 Called by the system to request the text content of an individual cell. You 9229 should pass the text into the provided `sink` delegate. This function will be 9230 called for each visible cell as-needed when drawing. 9231 +/ 9232 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9233 9234 /++ 9235 Available per-cell style customization options. Use one of the constructors 9236 provided to set the values conveniently, or default construct it and set individual 9237 values yourself. Just remember to set the `flags` so your values are actually used. 9238 If the flag isn't set, the field is ignored and the system default is used instead. 9239 9240 This is returned by the [getCellStyle] delegate. 9241 9242 Examples: 9243 --- 9244 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9245 auto table = new TableView(window); 9246 // snip: you would set up columns here 9247 9248 // this is how you provide data to the table view class 9249 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9250 import std.conv; 9251 sink(to!string(my_data[row][column])); 9252 }; 9253 9254 // and this is how you customize the colors 9255 table.getCellStyle = delegate(int row, int column) { 9256 return (my_data[row][column] < 0) ? 9257 TableView.CellStyle(Color.red); // make negative numbers red 9258 : TableView.CellStyle.init; // leave the rest alone 9259 }; 9260 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9261 --- 9262 9263 History: 9264 Added November 27, 2021 (dub v10.4) 9265 +/ 9266 struct CellStyle { 9267 /// 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. 9268 this(Color textColor) { 9269 this.textColor = textColor; 9270 this.flags |= Flags.textColorSet; 9271 } 9272 /// Sets a custom text and background color. 9273 this(Color textColor, Color backgroundColor) { 9274 this.textColor = textColor; 9275 this.backgroundColor = backgroundColor; 9276 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9277 } 9278 9279 Color textColor; 9280 Color backgroundColor; 9281 int flags; /// bitmask of [Flags] 9282 /// available options to combine into [flags] 9283 enum Flags { 9284 textColorSet = 1 << 0, 9285 backgroundColorSet = 1 << 1, 9286 } 9287 } 9288 /++ 9289 Companion delegate to [getData] that allows you to custom style each 9290 cell of the table. 9291 9292 Returns: 9293 A [CellStyle] structure that describes the desired style for the 9294 given cell. `return CellStyle.init` if you want the default style. 9295 9296 History: 9297 Added November 27, 2021 (dub v10.4) 9298 +/ 9299 CellStyle delegate(int row, int column) getCellStyle; 9300 9301 // i want to be able to do things like draw little colored things to show red for negative numbers 9302 // or background color indicators or even in-cell charts 9303 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9304 9305 /++ 9306 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9307 +/ 9308 mixin Emits!HeaderClickedEvent; 9309 } 9310 9311 /++ 9312 This is emitted by the [TableView] when a user clicks on a column header. 9313 9314 Its member `columnIndex` has the zero-based index of the column that was clicked. 9315 9316 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9317 9318 History: 9319 Added November 27, 2021 (dub v10.4) 9320 +/ 9321 class HeaderClickedEvent : Event { 9322 enum EventString = "HeaderClicked"; 9323 this(Widget target, int columnIndex) { 9324 this.columnIndex = columnIndex; 9325 super(EventString, target); 9326 } 9327 9328 /// The index of the column 9329 int columnIndex; 9330 9331 /// 9332 override @property int intValue() { 9333 return columnIndex; 9334 } 9335 } 9336 9337 version(custom_widgets) 9338 private class TableViewWidgetInner : Widget { 9339 9340 // wrap this thing in a ScrollMessageWidget 9341 9342 TableView tvw; 9343 ScrollMessageWidget smw; 9344 HeaderWidget header; 9345 9346 this(TableView tvw, ScrollMessageWidget smw) { 9347 this.tvw = tvw; 9348 this.smw = smw; 9349 super(smw); 9350 9351 this.tabStop = true; 9352 9353 header = new HeaderWidget(this, smw.getHeader()); 9354 9355 smw.addEventListener("scroll", () { 9356 this.redraw(); 9357 header.redraw(); 9358 }); 9359 9360 9361 // I need headers outside the scroll area but rendered on the same line as the up arrow 9362 // FIXME: add a fixed header to the SMW 9363 } 9364 9365 enum padding = 3; 9366 9367 void updateScrolls() { 9368 int w; 9369 foreach(idx, column; tvw.columns) { 9370 if(column.width == 0) continue; 9371 w += tvw.getActualSetSize(idx, false);// + padding; 9372 } 9373 smw.setTotalArea(w, tvw.itemCount); 9374 columnsWidth = w; 9375 } 9376 9377 private int columnsWidth; 9378 9379 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9380 9381 override void registerMovement() { 9382 super.registerMovement(); 9383 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9384 smw.setViewableArea(this.width, this.height / lh); 9385 } 9386 9387 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9388 int x; 9389 int y; 9390 9391 int row = smw.position.y; 9392 9393 foreach(lol; 0 .. this.height / lh) { 9394 if(row >= tvw.itemCount) 9395 break; 9396 x = 0; 9397 foreach(columnNumber, column; tvw.columns) { 9398 auto x2 = x + column.calculatedWidth; 9399 auto smwx = smw.position.x; 9400 9401 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9402 auto startX = x; 9403 auto endX = x + column.calculatedWidth; 9404 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9405 case TextAlignment.Left: startX += padding; break; 9406 case TextAlignment.Center: startX += padding; endX -= padding; break; 9407 case TextAlignment.Right: endX -= padding; break; 9408 default: /* broken */ break; 9409 } 9410 if(column.width != 0) // no point drawing an invisible column 9411 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9412 // auto clip = painter.setClipRectangle( 9413 9414 void dotext(WidgetPainter painter) { 9415 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9416 } 9417 9418 if(tvw.getCellStyle !is null) { 9419 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9420 9421 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9422 auto tempPainter = painter; 9423 tempPainter.fillColor = style.backgroundColor; 9424 tempPainter.outlineColor = style.backgroundColor; 9425 9426 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9427 Point(endX - smw.position.x, y + lh)); 9428 } 9429 auto tempPainter = painter; 9430 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9431 tempPainter.outlineColor = style.textColor; 9432 9433 dotext(tempPainter); 9434 } else { 9435 dotext(painter); 9436 } 9437 }); 9438 } 9439 9440 x += column.calculatedWidth; 9441 } 9442 row++; 9443 y += lh; 9444 } 9445 return bounds; 9446 } 9447 9448 static class Style : Widget.Style { 9449 override WidgetBackground background() { 9450 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9451 } 9452 } 9453 mixin OverrideStyle!Style; 9454 9455 private static class HeaderWidget : Widget { 9456 this(TableViewWidgetInner tvw, Widget parent) { 9457 super(parent); 9458 this.tvw = tvw; 9459 9460 this.remainder = new Button("", this); 9461 9462 this.addEventListener((scope ClickEvent ev) { 9463 int header = -1; 9464 foreach(idx, child; this.children[1 .. $]) { 9465 if(child is ev.target) { 9466 header = cast(int) idx; 9467 break; 9468 } 9469 } 9470 9471 if(header != -1) { 9472 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9473 hce.dispatch(); 9474 } 9475 9476 }); 9477 } 9478 9479 void updateHeaders() { 9480 foreach(child; children[1 .. $]) 9481 child.removeWidget(); 9482 9483 foreach(column; tvw.tvw.columns) { 9484 // the cast is ok because I dup it above, just the type is never changed. 9485 // all this is private so it should never get messed up. 9486 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9487 } 9488 } 9489 9490 Button remainder; 9491 TableViewWidgetInner tvw; 9492 9493 override void recomputeChildLayout() { 9494 registerMovement(); 9495 int pos; 9496 foreach(idx, child; children[1 .. $]) { 9497 if(idx >= tvw.tvw.columns.length) 9498 continue; 9499 child.x = pos; 9500 child.y = 0; 9501 child.width = tvw.tvw.columns[idx].calculatedWidth; 9502 child.height = scaleWithDpi(16);// this.height; 9503 pos += child.width; 9504 9505 child.recomputeChildLayout(); 9506 } 9507 9508 if(remainder is null) 9509 return; 9510 9511 remainder.x = pos; 9512 remainder.y = 0; 9513 if(pos < this.width) 9514 remainder.width = this.width - pos;// + 4; 9515 else 9516 remainder.width = 0; 9517 remainder.height = scaleWithDpi(16); 9518 9519 remainder.recomputeChildLayout(); 9520 } 9521 9522 // for the scrollable children mixin 9523 Point scrollOrigin() { 9524 return Point(tvw.smw.position.x, 0); 9525 } 9526 void paintFrameAndBackground(WidgetPainter painter) { } 9527 9528 mixin ScrollableChildren; 9529 } 9530 } 9531 9532 /+ 9533 9534 // given struct / array / number / string / etc, make it viewable and editable 9535 class DataViewerWidget : Widget { 9536 9537 } 9538 +/ 9539 9540 /++ 9541 A line edit box with an associated label. 9542 9543 History: 9544 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9545 9546 ``` 9547 Old: ________ 9548 9549 New: 9550 ____________ 9551 ``` 9552 9553 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9554 9555 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9556 horizontal label but left aligned. You may also consider a [GridLayout]. 9557 +/ 9558 alias LabeledLineEdit = Labeled!LineEdit; 9559 9560 /++ 9561 History: 9562 Added May 19, 2021 9563 +/ 9564 class Labeled(T) : Widget { 9565 /// 9566 this(string label, Widget parent) { 9567 super(parent); 9568 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9569 } 9570 9571 /++ 9572 History: 9573 The alignment parameter was added May 17, 2021 9574 +/ 9575 this(string label, TextAlignment alignment, Widget parent) { 9576 super(parent); 9577 initialize!HorizontalLayout(label, alignment, parent); 9578 } 9579 9580 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9581 tabStop = false; 9582 horizontal = is(L == HorizontalLayout); 9583 auto hl = new L(this); 9584 if(horizontal) { 9585 static class SpecialTextLabel : TextLabel { 9586 this(string label, TextAlignment alignment, Widget parent) { 9587 super(label, alignment, parent); 9588 } 9589 9590 override int paddingTop() { return 6; } 9591 } 9592 this.label = new SpecialTextLabel(label, alignment, hl); 9593 } else 9594 this.label = new TextLabel(label, alignment, hl); 9595 this.lineEdit = new T(hl); 9596 9597 this.label.labelFor = this.lineEdit; 9598 } 9599 9600 private bool horizontal; 9601 9602 TextLabel label; /// 9603 T lineEdit; /// 9604 9605 override int flexBasisWidth() { return 250; } 9606 9607 override int minHeight() { 9608 return this.children[0].minHeight; 9609 } 9610 override int maxHeight() { return minHeight(); } 9611 override int marginTop() { return 4; } 9612 override int marginBottom() { return 4; } 9613 9614 // FIXME: i should prolly call it value as well as content tbh 9615 9616 /// 9617 @property string content() { 9618 return lineEdit.content; 9619 } 9620 /// 9621 @property void content(string c) { 9622 return lineEdit.content(c); 9623 } 9624 9625 /// 9626 void selectAll() { 9627 lineEdit.selectAll(); 9628 } 9629 9630 override void focus() { 9631 lineEdit.focus(); 9632 } 9633 } 9634 9635 /++ 9636 A labeled password edit. 9637 9638 History: 9639 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9640 9641 The default parameters for the constructors were also removed on May 19, 2021 9642 +/ 9643 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9644 9645 private string toMenuLabel(string s) { 9646 string n; 9647 n.reserve(s.length); 9648 foreach(c; s) 9649 if(c == '_') 9650 n ~= ' '; 9651 else 9652 n ~= c; 9653 return n; 9654 } 9655 9656 private void autoExceptionHandler(Exception e) { 9657 messageBox(e.msg); 9658 } 9659 9660 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9661 static if(is(T : void delegate())) { 9662 return () { 9663 try 9664 t(); 9665 catch(Exception e) 9666 autoExceptionHandler(e); 9667 }; 9668 } else static if(is(typeof(fn) Params == __parameters)) { 9669 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9670 return () { 9671 void onOK(string s) { 9672 member = s; 9673 try 9674 t(Params[0](s)); 9675 catch(Exception e) 9676 autoExceptionHandler(e); 9677 } 9678 9679 if( 9680 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9681 || type == FileDialogType.Save) 9682 { 9683 getSaveFileName(&onOK, member, filters, null); 9684 } else 9685 getOpenFileName(&onOK, member, filters, null); 9686 }; 9687 } else { 9688 struct S { 9689 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9690 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9691 } else mixin(q{ 9692 static foreach(idx, ignore; Params) { 9693 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9694 } 9695 }); 9696 } 9697 return () { 9698 dialog((S s) { 9699 try { 9700 static if(is(typeof(t) Ret == return)) { 9701 static if(is(Ret == void)) { 9702 t(s.tupleof); 9703 } else { 9704 auto ret = t(s.tupleof); 9705 import std.conv; 9706 messageBox(to!string(ret), "Returned Value"); 9707 } 9708 } 9709 } catch(Exception e) 9710 autoExceptionHandler(e); 9711 }, null, __traits(identifier, fn)); 9712 }; 9713 } 9714 } 9715 } 9716 9717 private template hasAnyRelevantAnnotations(a...) { 9718 bool helper() { 9719 bool any; 9720 foreach(attr; a) { 9721 static if(is(typeof(attr) == .menu)) 9722 any = true; 9723 else static if(is(typeof(attr) == .toolbar)) 9724 any = true; 9725 else static if(is(attr == .separator)) 9726 any = true; 9727 else static if(is(typeof(attr) == .accelerator)) 9728 any = true; 9729 else static if(is(typeof(attr) == .hotkey)) 9730 any = true; 9731 else static if(is(typeof(attr) == .icon)) 9732 any = true; 9733 else static if(is(typeof(attr) == .label)) 9734 any = true; 9735 else static if(is(typeof(attr) == .tip)) 9736 any = true; 9737 } 9738 return any; 9739 } 9740 9741 enum bool hasAnyRelevantAnnotations = helper(); 9742 } 9743 9744 /++ 9745 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. 9746 +/ 9747 class MainWindow : Window { 9748 /// 9749 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9750 super(initialWidth, initialHeight, title); 9751 9752 _clientArea = new ClientAreaWidget(); 9753 _clientArea.x = 0; 9754 _clientArea.y = 0; 9755 _clientArea.width = this.width; 9756 _clientArea.height = this.height; 9757 _clientArea.tabStop = false; 9758 9759 super.addChild(_clientArea); 9760 9761 statusBar = new StatusBar(this); 9762 } 9763 9764 /++ 9765 Adds a menu and toolbar from annotated functions. 9766 9767 --- 9768 struct Commands { 9769 @menu("File") { 9770 void New() {} 9771 void Open() {} 9772 void Save() {} 9773 @separator 9774 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9775 window.close(); 9776 } 9777 } 9778 9779 @menu("Edit") { 9780 void Undo() { 9781 undo(); 9782 } 9783 @separator 9784 void Cut() {} 9785 void Copy() {} 9786 void Paste() {} 9787 } 9788 9789 @menu("Help") { 9790 void About() {} 9791 } 9792 } 9793 9794 Commands commands; 9795 9796 window.setMenuAndToolbarFromAnnotatedCode(commands); 9797 --- 9798 9799 Note that you can call this function multiple times and it will add the items in order to the given items. 9800 9801 +/ 9802 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9803 setMenuAndToolbarFromAnnotatedCode_internal(t); 9804 } 9805 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9806 setMenuAndToolbarFromAnnotatedCode_internal(t); 9807 } 9808 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9809 Action[] toolbarActions; 9810 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9811 Menu[string] mcs; 9812 9813 foreach(menu; menuBar.subMenus) { 9814 mcs[menu.label] = menu; 9815 } 9816 9817 foreach(memberName; __traits(derivedMembers, T)) { 9818 static if(memberName != "this") 9819 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9820 .menu menu; 9821 .toolbar toolbar; 9822 bool separator; 9823 .accelerator accelerator; 9824 .hotkey hotkey; 9825 .icon icon; 9826 string label; 9827 string tip; 9828 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9829 static if(is(typeof(attr) == .menu)) 9830 menu = attr; 9831 else static if(is(typeof(attr) == .toolbar)) 9832 toolbar = attr; 9833 else static if(is(attr == .separator)) 9834 separator = true; 9835 else static if(is(typeof(attr) == .accelerator)) 9836 accelerator = attr; 9837 else static if(is(typeof(attr) == .hotkey)) 9838 hotkey = attr; 9839 else static if(is(typeof(attr) == .icon)) 9840 icon = attr; 9841 else static if(is(typeof(attr) == .label)) 9842 label = attr.label; 9843 else static if(is(typeof(attr) == .tip)) 9844 tip = attr.tip; 9845 } 9846 9847 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9848 ushort correctIcon = icon.id; // FIXME 9849 if(label.length == 0) 9850 label = memberName.toMenuLabel; 9851 9852 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9853 9854 auto action = new Action(label, correctIcon, handler); 9855 9856 if(accelerator.keyString.length) { 9857 auto ke = KeyEvent.parse(accelerator.keyString); 9858 action.accelerator = ke; 9859 accelerators[ke.toStr] = handler; 9860 } 9861 9862 if(toolbar !is .toolbar.init) 9863 toolbarActions ~= action; 9864 if(menu !is .menu.init) { 9865 Menu mc; 9866 if(menu.name in mcs) { 9867 mc = mcs[menu.name]; 9868 } else { 9869 mc = new Menu(menu.name, this); 9870 menuBar.addItem(mc); 9871 mcs[menu.name] = mc; 9872 } 9873 9874 if(separator) 9875 mc.addSeparator(); 9876 mc.addItem(new MenuItem(action)); 9877 } 9878 } 9879 } 9880 } 9881 9882 this.menuBar = menuBar; 9883 9884 if(toolbarActions.length) { 9885 auto tb = new ToolBar(toolbarActions, this); 9886 } 9887 } 9888 9889 void delegate()[string] accelerators; 9890 9891 override void defaultEventHandler_keydown(KeyDownEvent event) { 9892 auto str = event.originalKeyEvent.toStr; 9893 if(auto acl = str in accelerators) 9894 (*acl)(); 9895 super.defaultEventHandler_keydown(event); 9896 } 9897 9898 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9899 super.defaultEventHandler_mouseover(event); 9900 if(this.statusBar !is null && event.target.statusTip.length) 9901 this.statusBar.parts[0].content = event.target.statusTip; 9902 else if(this.statusBar !is null && this.statusTip.length) 9903 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9904 } 9905 9906 override void addChild(Widget c, int position = int.max) { 9907 if(auto tb = cast(ToolBar) c) 9908 version(win32_widgets) 9909 super.addChild(c, 0); 9910 else version(custom_widgets) 9911 super.addChild(c, menuBar ? 1 : 0); 9912 else static assert(0); 9913 else 9914 clientArea.addChild(c, position); 9915 } 9916 9917 ToolBar _toolBar; 9918 /// 9919 ToolBar toolBar() { return _toolBar; } 9920 /// 9921 ToolBar toolBar(ToolBar t) { 9922 _toolBar = t; 9923 foreach(child; this.children) 9924 if(child is t) 9925 return t; 9926 version(win32_widgets) 9927 super.addChild(t, 0); 9928 else version(custom_widgets) 9929 super.addChild(t, menuBar ? 1 : 0); 9930 else static assert(0); 9931 return t; 9932 } 9933 9934 MenuBar _menu; 9935 /// 9936 MenuBar menuBar() { return _menu; } 9937 /// 9938 MenuBar menuBar(MenuBar m) { 9939 if(m is _menu) { 9940 version(custom_widgets) 9941 recomputeChildLayout(); 9942 return m; 9943 } 9944 9945 if(_menu !is null) { 9946 // make sure it is sanely removed 9947 // FIXME 9948 } 9949 9950 _menu = m; 9951 9952 version(win32_widgets) { 9953 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9954 } else version(custom_widgets) { 9955 super.addChild(m, 0); 9956 9957 // clientArea.y = menu.height; 9958 // clientArea.height = this.height - menu.height; 9959 9960 recomputeChildLayout(); 9961 } else static assert(false); 9962 9963 return _menu; 9964 } 9965 private Widget _clientArea; 9966 /// 9967 @property Widget clientArea() { return _clientArea; } 9968 protected @property void clientArea(Widget wid) { 9969 _clientArea = wid; 9970 } 9971 9972 private StatusBar _statusBar; 9973 /++ 9974 Returns the window's [StatusBar]. Be warned it may be `null`. 9975 +/ 9976 @property StatusBar statusBar() { return _statusBar; } 9977 /// ditto 9978 @property void statusBar(StatusBar bar) { 9979 if(_statusBar !is null) 9980 _statusBar.removeWidget(); 9981 _statusBar = bar; 9982 if(bar !is null) 9983 super.addChild(_statusBar); 9984 } 9985 } 9986 9987 /+ 9988 This is really an implementation detail of [MainWindow] 9989 +/ 9990 private class ClientAreaWidget : Widget { 9991 this() { 9992 this.tabStop = false; 9993 super(null); 9994 //sa = new ScrollableWidget(this); 9995 } 9996 /* 9997 ScrollableWidget sa; 9998 override void addChild(Widget w, int position) { 9999 if(sa is null) 10000 super.addChild(w, position); 10001 else { 10002 sa.addChild(w, position); 10003 sa.setContentSize(this.minWidth + 1, this.minHeight); 10004 writeln(sa.contentWidth, "x", sa.contentHeight); 10005 } 10006 } 10007 */ 10008 } 10009 10010 /** 10011 Toolbars are lists of buttons (typically icons) that appear under the menu. 10012 Each button ought to correspond to a menu item, represented by [Action] objects. 10013 */ 10014 class ToolBar : Widget { 10015 version(win32_widgets) { 10016 private int idealHeight; 10017 override int minHeight() { return idealHeight; } 10018 override int maxHeight() { return idealHeight; } 10019 } else version(custom_widgets) { 10020 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10021 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10022 } else static assert(false); 10023 override int heightStretchiness() { return 0; } 10024 10025 version(win32_widgets) { 10026 HIMAGELIST imageListSmall; 10027 HIMAGELIST imageListLarge; 10028 } 10029 10030 this(Widget parent) { 10031 this(null, parent); 10032 } 10033 10034 version(win32_widgets) 10035 void changeIconSize(bool useLarge) { 10036 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10037 10038 /+ 10039 SIZE size; 10040 import core.sys.windows.commctrl; 10041 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10042 idealHeight = size.cy + 4; // the plus 4 is a hack 10043 +/ 10044 10045 idealHeight = useLarge ? 34 : 26; 10046 10047 if(parent) { 10048 parent.recomputeChildLayout(); 10049 parent.redraw(); 10050 } 10051 10052 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10053 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10054 } 10055 10056 /// 10057 this(Action[] actions, Widget parent) { 10058 super(parent); 10059 10060 tabStop = false; 10061 10062 version(win32_widgets) { 10063 // so i like how the flat thing looks on windows, but not on wine 10064 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10065 // leave it commented 10066 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 10067 10068 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 10069 10070 imageListSmall = ImageList_Create( 10071 // width, height 10072 16, 16, 10073 ILC_COLOR16 | ILC_MASK, 10074 16 /*numberOfButtons*/, 0); 10075 10076 imageListLarge = ImageList_Create( 10077 // width, height 10078 24, 24, 10079 ILC_COLOR16 | ILC_MASK, 10080 16 /*numberOfButtons*/, 0); 10081 10082 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 10083 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10084 10085 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10086 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10087 10088 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10089 10090 TBBUTTON[] buttons; 10091 10092 // FIXME: I_IMAGENONE is if here is no icon 10093 foreach(action; actions) 10094 buttons ~= TBBUTTON( 10095 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10096 action.id, 10097 TBSTATE_ENABLED, // state 10098 0, // style 10099 0, // reserved array, just zero it out 10100 0, // dwData 10101 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10102 ); 10103 10104 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10105 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10106 10107 /* 10108 RECT rect; 10109 GetWindowRect(hwnd, &rect); 10110 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 10111 */ 10112 10113 dpiChanged(); // to load the things calling changeIconSize the first time 10114 10115 assert(idealHeight); 10116 } else version(custom_widgets) { 10117 foreach(action; actions) 10118 new ToolButton(action, this); 10119 } else static assert(false); 10120 } 10121 10122 override void recomputeChildLayout() { 10123 .recomputeChildLayout!"width"(this); 10124 } 10125 10126 10127 version(win32_widgets) 10128 override protected void dpiChanged() { 10129 auto sz = scaleWithDpi(16); 10130 if(sz >= 20) 10131 changeIconSize(true); 10132 else 10133 changeIconSize(false); 10134 } 10135 } 10136 10137 enum toolbarIconSize = 24; 10138 10139 /// 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. 10140 class ToolButton : Button { 10141 /// 10142 this(string label, Widget parent) { 10143 super(label, parent); 10144 tabStop = false; 10145 } 10146 /// 10147 this(Action action, Widget parent) { 10148 super(action.label, parent); 10149 tabStop = false; 10150 this.action = action; 10151 } 10152 10153 version(custom_widgets) 10154 override void defaultEventHandler_click(ClickEvent event) { 10155 foreach(handler; action.triggered) 10156 handler(); 10157 } 10158 10159 Action action; 10160 10161 override int maxWidth() { return toolbarIconSize; } 10162 override int minWidth() { return toolbarIconSize; } 10163 override int maxHeight() { return toolbarIconSize; } 10164 override int minHeight() { return toolbarIconSize; } 10165 10166 version(custom_widgets) 10167 override void paint(WidgetPainter painter) { 10168 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10169 painter.outlineColor = Color.black; 10170 10171 // I want to get from 16 to 24. that's * 3 / 2 10172 static assert(toolbarIconSize >= 16); 10173 enum multiplier = toolbarIconSize / 8; 10174 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10175 switch(action.iconId) { 10176 case GenericIcons.New: 10177 painter.fillColor = Color.white; 10178 painter.drawPolygon( 10179 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10180 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10181 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10182 ); 10183 break; 10184 case GenericIcons.Save: 10185 painter.fillColor = Color.white; 10186 painter.outlineColor = Color.black; 10187 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10188 10189 // the label 10190 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10191 10192 // the slider 10193 painter.fillColor = Color.black; 10194 painter.outlineColor = Color.black; 10195 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10196 10197 painter.fillColor = Color.white; 10198 painter.outlineColor = Color.white; 10199 // the disc window 10200 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10201 break; 10202 case GenericIcons.Open: 10203 painter.fillColor = Color.white; 10204 painter.drawPolygon( 10205 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10206 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10207 painter.drawPolygon( 10208 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10209 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10210 Point(2, 6) * multiplier / divisor); 10211 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10212 break; 10213 case GenericIcons.Copy: 10214 painter.fillColor = Color.white; 10215 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10216 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10217 break; 10218 case GenericIcons.Cut: 10219 painter.fillColor = Color.transparent; 10220 painter.outlineColor = getComputedStyle.foregroundColor(); 10221 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10222 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10223 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10224 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10225 break; 10226 case GenericIcons.Paste: 10227 painter.fillColor = Color.white; 10228 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10229 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10230 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10231 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10232 painter.fillColor = Color.black; 10233 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10234 break; 10235 case GenericIcons.Help: 10236 painter.outlineColor = getComputedStyle.foregroundColor(); 10237 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10238 break; 10239 case GenericIcons.Undo: 10240 painter.fillColor = Color.transparent; 10241 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10242 painter.outlineColor = Color.black; 10243 painter.fillColor = Color.black; 10244 painter.drawPolygon( 10245 Point(4, 4) * multiplier / divisor, 10246 Point(8, 2) * multiplier / divisor, 10247 Point(8, 6) * multiplier / divisor, 10248 Point(4, 4) * multiplier / divisor, 10249 ); 10250 break; 10251 case GenericIcons.Redo: 10252 painter.fillColor = Color.transparent; 10253 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10254 painter.outlineColor = Color.black; 10255 painter.fillColor = Color.black; 10256 painter.drawPolygon( 10257 Point(10, 4) * multiplier / divisor, 10258 Point(6, 2) * multiplier / divisor, 10259 Point(6, 6) * multiplier / divisor, 10260 Point(10, 4) * multiplier / divisor, 10261 ); 10262 break; 10263 default: 10264 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10265 } 10266 return bounds; 10267 }); 10268 } 10269 10270 } 10271 10272 10273 /++ 10274 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 10275 +/ 10276 class MenuBar : Widget { 10277 MenuItem[] items; 10278 Menu[] subMenus; 10279 10280 version(win32_widgets) { 10281 HMENU handle; 10282 /// 10283 this(Widget parent = null) { 10284 super(parent); 10285 10286 handle = CreateMenu(); 10287 tabStop = false; 10288 } 10289 } else version(custom_widgets) { 10290 /// 10291 this(Widget parent = null) { 10292 tabStop = false; // these are selected some other way 10293 super(parent); 10294 } 10295 10296 mixin Padding!q{2}; 10297 } else static assert(false); 10298 10299 version(custom_widgets) 10300 override void paint(WidgetPainter painter) { 10301 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10302 } 10303 10304 /// 10305 MenuItem addItem(MenuItem item) { 10306 this.addChild(item); 10307 items ~= item; 10308 version(win32_widgets) { 10309 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10310 } 10311 return item; 10312 } 10313 10314 10315 /// 10316 Menu addItem(Menu item) { 10317 10318 subMenus ~= item; 10319 10320 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10321 10322 addChild(mbItem); 10323 items ~= mbItem; 10324 10325 version(win32_widgets) { 10326 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10327 } else version(custom_widgets) { 10328 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10329 item.popup(mbItem); 10330 }; 10331 } else static assert(false); 10332 10333 return item; 10334 } 10335 10336 override void recomputeChildLayout() { 10337 .recomputeChildLayout!"width"(this); 10338 } 10339 10340 override int maxHeight() { return defaultLineHeight + 4; } 10341 override int minHeight() { return defaultLineHeight + 4; } 10342 } 10343 10344 10345 /** 10346 Status bars appear at the bottom of a MainWindow. 10347 They are made out of Parts, with a width and content. 10348 10349 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10350 10351 10352 sb.parts[0].content = "Status bar text!"; 10353 */ 10354 class StatusBar : Widget { 10355 private Part[] partsArray; 10356 /// 10357 struct Parts { 10358 @disable this(); 10359 this(StatusBar owner) { this.owner = owner; } 10360 //@disable this(this); 10361 /// 10362 @property int length() { return cast(int) owner.partsArray.length; } 10363 private StatusBar owner; 10364 private this(StatusBar owner, Part[] parts) { 10365 this.owner.partsArray = parts; 10366 this.owner = owner; 10367 } 10368 /// 10369 Part opIndex(int p) { 10370 if(owner.partsArray.length == 0) 10371 this ~= new StatusBar.Part(0); 10372 return owner.partsArray[p]; 10373 } 10374 10375 /// 10376 Part opOpAssign(string op : "~" )(Part p) { 10377 assert(owner.partsArray.length < 255); 10378 p.owner = this.owner; 10379 p.idx = cast(int) owner.partsArray.length; 10380 owner.partsArray ~= p; 10381 10382 owner.recomputeChildLayout(); 10383 10384 version(win32_widgets) { 10385 int[256] pos; 10386 int cpos; 10387 foreach(idx, part; owner.partsArray) { 10388 if(idx + 1 == owner.partsArray.length) 10389 pos[idx] = -1; 10390 else { 10391 cpos += part.currentlyAssignedWidth; 10392 pos[idx] = cpos; 10393 } 10394 } 10395 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10396 } else version(custom_widgets) { 10397 owner.redraw(); 10398 } else static assert(false); 10399 10400 return p; 10401 } 10402 } 10403 10404 private Parts _parts; 10405 /// 10406 final @property Parts parts() { 10407 return _parts; 10408 } 10409 10410 /++ 10411 10412 +/ 10413 static class Part { 10414 /++ 10415 History: 10416 Added September 1, 2023 (dub v11.1) 10417 +/ 10418 enum WidthUnits { 10419 /++ 10420 Unscaled pixels as they appear on screen. 10421 10422 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 10423 +/ 10424 DeviceDependentPixels, 10425 /++ 10426 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 10427 +/ 10428 DeviceIndependentPixels, 10429 /++ 10430 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`). 10431 +/ 10432 ApproximateCharacters, 10433 /++ 10434 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. 10435 10436 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. 10437 +/ 10438 Proportional 10439 } 10440 private WidthUnits units; 10441 private int width; 10442 private StatusBar owner; 10443 10444 private int currentlyAssignedWidth; 10445 10446 /++ 10447 History: 10448 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. 10449 10450 It now allows you to provide your own value for [WidthUnits]. 10451 10452 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`. 10453 +/ 10454 this(int w, WidthUnits units = WidthUnits.Proportional) { 10455 this.units = units; 10456 this.width = w; 10457 } 10458 10459 /// ditto 10460 this(int w = 0) { 10461 if(w == 0) 10462 this(w, WidthUnits.Proportional); 10463 else 10464 this(w, WidthUnits.DeviceDependentPixels); 10465 } 10466 10467 private int idx; 10468 private string _content; 10469 /// 10470 @property string content() { return _content; } 10471 /// 10472 @property void content(string s) { 10473 version(win32_widgets) { 10474 _content = s; 10475 WCharzBuffer bfr = WCharzBuffer(s); 10476 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10477 } else version(custom_widgets) { 10478 if(_content != s) { 10479 _content = s; 10480 owner.redraw(); 10481 } 10482 } else static assert(false); 10483 } 10484 } 10485 string simpleModeContent; 10486 bool inSimpleMode; 10487 10488 10489 /// 10490 this(Widget parent) { 10491 super(null); // FIXME 10492 _parts = Parts(this); 10493 tabStop = false; 10494 version(win32_widgets) { 10495 parentWindow = parent.parentWindow; 10496 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10497 10498 RECT rect; 10499 GetWindowRect(hwnd, &rect); 10500 idealHeight = rect.bottom - rect.top; 10501 assert(idealHeight); 10502 } else version(custom_widgets) { 10503 } else static assert(false); 10504 } 10505 10506 override void recomputeChildLayout() { 10507 int remainingLength = this.width; 10508 10509 int proportionalSum; 10510 int proportionalCount; 10511 foreach(idx, part; this.partsArray) { 10512 with(Part.WidthUnits) 10513 final switch(part.units) { 10514 case DeviceDependentPixels: 10515 part.currentlyAssignedWidth = part.width; 10516 remainingLength -= part.currentlyAssignedWidth; 10517 break; 10518 case DeviceIndependentPixels: 10519 part.currentlyAssignedWidth = scaleWithDpi(part.width); 10520 remainingLength -= part.currentlyAssignedWidth; 10521 break; 10522 case ApproximateCharacters: 10523 auto cs = getComputedStyle(); 10524 auto font = cs.font; 10525 10526 part.currentlyAssignedWidth = font.averageWidth * this.width; 10527 remainingLength -= part.currentlyAssignedWidth; 10528 break; 10529 case Proportional: 10530 proportionalSum += part.width; 10531 proportionalCount ++; 10532 break; 10533 } 10534 } 10535 10536 foreach(part; this.partsArray) { 10537 if(part.units == Part.WidthUnits.Proportional) { 10538 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 10539 if(proportion == 0) 10540 proportion = 1; 10541 10542 if(proportionalSum == 0) 10543 proportionalSum = proportionalCount; 10544 10545 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 10546 } 10547 } 10548 10549 super.recomputeChildLayout(); 10550 } 10551 10552 version(win32_widgets) 10553 override protected void dpiChanged() { 10554 RECT rect; 10555 GetWindowRect(hwnd, &rect); 10556 idealHeight = rect.bottom - rect.top; 10557 assert(idealHeight); 10558 } 10559 10560 version(custom_widgets) 10561 override void paint(WidgetPainter painter) { 10562 auto cs = getComputedStyle(); 10563 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10564 int cpos = 0; 10565 foreach(idx, part; this.partsArray) { 10566 auto partWidth = part.currentlyAssignedWidth; 10567 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10568 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10569 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10570 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10571 10572 painter.outlineColor = cs.foregroundColor(); 10573 painter.fillColor = cs.foregroundColor(); 10574 10575 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10576 cpos += partWidth; 10577 } 10578 } 10579 10580 10581 version(win32_widgets) { 10582 private int idealHeight; 10583 override int maxHeight() { return idealHeight; } 10584 override int minHeight() { return idealHeight; } 10585 } else version(custom_widgets) { 10586 override int maxHeight() { return defaultLineHeight + 4; } 10587 override int minHeight() { return defaultLineHeight + 4; } 10588 } else static assert(false); 10589 } 10590 10591 /// Displays an in-progress indicator without known values 10592 version(none) 10593 class IndefiniteProgressBar : Widget { 10594 version(win32_widgets) 10595 this(Widget parent) { 10596 super(parent); 10597 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10598 tabStop = false; 10599 } 10600 override int minHeight() { return 10; } 10601 } 10602 10603 /// A progress bar with a known endpoint and completion amount 10604 class ProgressBar : Widget { 10605 /++ 10606 History: 10607 Added March 16, 2022 (dub v10.7) 10608 +/ 10609 this(int min, int max, Widget parent) { 10610 this(parent); 10611 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10612 } 10613 this(Widget parent) { 10614 version(win32_widgets) { 10615 super(parent); 10616 createWin32Window(this, "msctls_progress32"w, "", 0); 10617 tabStop = false; 10618 } else version(custom_widgets) { 10619 super(parent); 10620 max = 100; 10621 step = 10; 10622 tabStop = false; 10623 } else static assert(0); 10624 } 10625 10626 version(custom_widgets) 10627 override void paint(WidgetPainter painter) { 10628 auto cs = getComputedStyle(); 10629 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10630 painter.fillColor = cs.progressBarColor; 10631 painter.drawRectangle(Point(0, 0), width * current / max, height); 10632 } 10633 10634 10635 version(custom_widgets) { 10636 int current; 10637 int max; 10638 int step; 10639 } 10640 10641 /// 10642 void advanceOneStep() { 10643 version(win32_widgets) 10644 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10645 else version(custom_widgets) 10646 addToPosition(step); 10647 else static assert(false); 10648 } 10649 10650 /// 10651 void setStepIncrement(int increment) { 10652 version(win32_widgets) 10653 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10654 else version(custom_widgets) 10655 step = increment; 10656 else static assert(false); 10657 } 10658 10659 /// 10660 void addToPosition(int amount) { 10661 version(win32_widgets) 10662 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10663 else version(custom_widgets) 10664 setPosition(current + amount); 10665 else static assert(false); 10666 } 10667 10668 /// 10669 void setPosition(int pos) { 10670 version(win32_widgets) 10671 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10672 else version(custom_widgets) { 10673 current = pos; 10674 if(current > max) 10675 current = max; 10676 redraw(); 10677 } 10678 else static assert(false); 10679 } 10680 10681 /// 10682 void setRange(ushort min, ushort max) { 10683 version(win32_widgets) 10684 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10685 else version(custom_widgets) { 10686 this.max = max; 10687 } 10688 else static assert(false); 10689 } 10690 10691 override int minHeight() { return 10; } 10692 } 10693 10694 version(custom_widgets) 10695 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10696 thisLabel.reserve(label.length); 10697 bool justSawAmpersand; 10698 foreach(ch; label) { 10699 if(justSawAmpersand) { 10700 justSawAmpersand = false; 10701 if(ch == '&') { 10702 goto plain; 10703 } 10704 thisAccelerator = ch; 10705 } else { 10706 if(ch == '&') { 10707 justSawAmpersand = true; 10708 continue; 10709 } 10710 plain: 10711 thisLabel ~= ch; 10712 } 10713 } 10714 } 10715 10716 /++ 10717 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. 10718 10719 10720 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 10721 10722 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10723 10724 History: 10725 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. 10726 +/ 10727 class Fieldset : Widget { 10728 // FIXME: on Windows,it doesn't draw the background on the label 10729 // on X, it doesn't fix the clipping rectangle for it 10730 version(win32_widgets) 10731 override int paddingTop() { return defaultLineHeight; } 10732 else version(custom_widgets) 10733 override int paddingTop() { return defaultLineHeight + 2; } 10734 else static assert(false); 10735 override int paddingBottom() { return 6; } 10736 override int paddingLeft() { return 6; } 10737 override int paddingRight() { return 6; } 10738 10739 override int marginLeft() { return 6; } 10740 override int marginRight() { return 6; } 10741 override int marginTop() { return 2; } 10742 override int marginBottom() { return 2; } 10743 10744 string legend; 10745 10746 version(custom_widgets) private dchar accelerator; 10747 10748 this(string legend, Widget parent) { 10749 version(win32_widgets) { 10750 super(parent); 10751 this.legend = legend; 10752 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10753 tabStop = false; 10754 } else version(custom_widgets) { 10755 super(parent); 10756 tabStop = false; 10757 10758 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10759 } else static assert(0); 10760 } 10761 10762 version(custom_widgets) 10763 override void paint(WidgetPainter painter) { 10764 auto dlh = defaultLineHeight; 10765 10766 painter.fillColor = Color.transparent; 10767 auto cs = getComputedStyle(); 10768 painter.pen = Pen(cs.foregroundColor, 1); 10769 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 10770 10771 auto tx = painter.textSize(legend); 10772 painter.outlineColor = Color.transparent; 10773 10774 version(Windows) { 10775 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10776 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10777 SelectObject(painter.impl.hdc, b); 10778 } else static if(UsingSimpledisplayX11) { 10779 painter.fillColor = getComputedStyle().windowBackgroundColor; 10780 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10781 } 10782 painter.outlineColor = cs.foregroundColor; 10783 painter.drawText(Point(8, 0), legend); 10784 } 10785 10786 override int maxHeight() { 10787 auto m = paddingTop() + paddingBottom(); 10788 foreach(child; children) { 10789 auto mh = child.maxHeight(); 10790 if(mh == int.max) 10791 return int.max; 10792 m += mh; 10793 m += child.marginBottom(); 10794 m += child.marginTop(); 10795 } 10796 m += 6; 10797 if(m < minHeight) 10798 return minHeight; 10799 return m; 10800 } 10801 10802 override int minHeight() { 10803 auto m = paddingTop() + paddingBottom(); 10804 foreach(child; children) { 10805 m += child.minHeight(); 10806 m += child.marginBottom(); 10807 m += child.marginTop(); 10808 } 10809 return m + 6; 10810 } 10811 10812 override int minWidth() { 10813 return 6 + cast(int) this.legend.length * 7; 10814 } 10815 } 10816 10817 /++ 10818 $(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") 10819 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10820 +/ 10821 version(minigui_screenshots) 10822 @Screenshot("Fieldset") 10823 unittest { 10824 auto window = new Window(200, 100); 10825 auto set = new Fieldset("Baby will", window); 10826 auto option1 = new Radiobox("Eat", set); 10827 auto option2 = new Radiobox("Cry", set); 10828 auto option3 = new Radiobox("Sleep", set); 10829 window.loop(); 10830 } 10831 10832 /// Draws a line 10833 class HorizontalRule : Widget { 10834 mixin Margin!q{ 2 }; 10835 override int minHeight() { return 2; } 10836 override int maxHeight() { return 2; } 10837 10838 /// 10839 this(Widget parent) { 10840 super(parent); 10841 } 10842 10843 override void paint(WidgetPainter painter) { 10844 auto cs = getComputedStyle(); 10845 painter.outlineColor = cs.darkAccentColor; 10846 painter.drawLine(Point(0, 0), Point(width, 0)); 10847 painter.outlineColor = cs.lightAccentColor; 10848 painter.drawLine(Point(0, 1), Point(width, 1)); 10849 } 10850 } 10851 10852 version(minigui_screenshots) 10853 @Screenshot("HorizontalRule") 10854 /++ 10855 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10856 10857 +/ 10858 unittest { 10859 auto window = new Window(200, 100); 10860 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10861 new HorizontalRule(window); 10862 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10863 window.loop(); 10864 } 10865 10866 /// ditto 10867 class VerticalRule : Widget { 10868 mixin Margin!q{ 2 }; 10869 override int minWidth() { return 2; } 10870 override int maxWidth() { return 2; } 10871 10872 /// 10873 this(Widget parent) { 10874 super(parent); 10875 } 10876 10877 override void paint(WidgetPainter painter) { 10878 auto cs = getComputedStyle(); 10879 painter.outlineColor = cs.darkAccentColor; 10880 painter.drawLine(Point(0, 0), Point(0, height)); 10881 painter.outlineColor = cs.lightAccentColor; 10882 painter.drawLine(Point(1, 0), Point(1, height)); 10883 } 10884 } 10885 10886 10887 /// 10888 class Menu : Window { 10889 void remove() { 10890 foreach(i, child; parentWindow.children) 10891 if(child is this) { 10892 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10893 break; 10894 } 10895 parentWindow.redraw(); 10896 10897 parentWindow.releaseMouseCapture(); 10898 } 10899 10900 /// 10901 void addSeparator() { 10902 version(win32_widgets) 10903 AppendMenu(handle, MF_SEPARATOR, 0, null); 10904 else version(custom_widgets) 10905 auto hr = new HorizontalRule(this); 10906 else static assert(0); 10907 } 10908 10909 override int paddingTop() { return 4; } 10910 override int paddingBottom() { return 4; } 10911 override int paddingLeft() { return 2; } 10912 override int paddingRight() { return 2; } 10913 10914 version(win32_widgets) {} 10915 else version(custom_widgets) { 10916 SimpleWindow dropDown; 10917 Widget menuParent; 10918 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10919 this.menuParent = parent; 10920 10921 int w = 150; 10922 int h = paddingTop + paddingBottom; 10923 if(this.children.length) { 10924 // hacking it to get the ideal height out of recomputeChildLayout 10925 this.width = w; 10926 this.height = h; 10927 this.recomputeChildLayout(); 10928 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10929 h += paddingBottom; 10930 10931 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 10932 } 10933 10934 if(offsetY == int.min) 10935 offsetY = parent.defaultLineHeight; 10936 10937 auto coord = parent.globalCoordinates(); 10938 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10939 this.x = 0; 10940 this.y = 0; 10941 this.width = dropDown.width; 10942 this.height = dropDown.height; 10943 this.drawableWindow = dropDown; 10944 this.recomputeChildLayout(); 10945 10946 static if(UsingSimpledisplayX11) 10947 XSync(XDisplayConnection.get, 0); 10948 10949 dropDown.visibilityChanged = (bool visible) { 10950 if(visible) { 10951 this.redraw(); 10952 dropDown.grabInput(); 10953 } else { 10954 dropDown.releaseInputGrab(); 10955 } 10956 }; 10957 10958 dropDown.show(); 10959 10960 clickListener = this.addEventListener((scope ClickEvent ev) { 10961 unpopup(); 10962 // need to unlock asap just in case other user handlers block... 10963 static if(UsingSimpledisplayX11) 10964 flushGui(); 10965 }, true /* again for asap action */); 10966 } 10967 10968 EventListener clickListener; 10969 } 10970 else static assert(false); 10971 10972 version(custom_widgets) 10973 void unpopup() { 10974 mouseLastOver = mouseLastDownOn = null; 10975 dropDown.hide(); 10976 if(!menuParent.parentWindow.win.closed) { 10977 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10978 maw.setDynamicState(DynamicState.depressed, false); 10979 maw.setDynamicState(DynamicState.hover, false); 10980 maw.redraw(); 10981 } 10982 // menuParent.parentWindow.win.focus(); 10983 } 10984 clickListener.disconnect(); 10985 } 10986 10987 MenuItem[] items; 10988 10989 /// 10990 MenuItem addItem(MenuItem item) { 10991 addChild(item); 10992 items ~= item; 10993 version(win32_widgets) { 10994 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10995 } 10996 return item; 10997 } 10998 10999 string label; 11000 11001 version(win32_widgets) { 11002 HMENU handle; 11003 /// 11004 this(string label, Widget parent) { 11005 // not actually passing the parent since it effs up the drawing 11006 super(cast(Widget) null);// parent); 11007 this.label = label; 11008 handle = CreatePopupMenu(); 11009 } 11010 } else version(custom_widgets) { 11011 /// 11012 this(string label, Widget parent) { 11013 11014 if(dropDown) { 11015 dropDown.close(); 11016 } 11017 dropDown = new SimpleWindow( 11018 150, 4, 11019 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11020 11021 this.label = label; 11022 11023 super(dropDown); 11024 } 11025 } else static assert(false); 11026 11027 override int maxHeight() { return defaultLineHeight; } 11028 override int minHeight() { return defaultLineHeight; } 11029 11030 version(custom_widgets) 11031 override void paint(WidgetPainter painter) { 11032 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 11033 } 11034 } 11035 11036 /++ 11037 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 11038 +/ 11039 class MenuItem : MouseActivatedWidget { 11040 Menu submenu; 11041 11042 Action action; 11043 string label; 11044 11045 override int paddingLeft() { return 4; } 11046 11047 override int maxHeight() { return defaultLineHeight + 4; } 11048 override int minHeight() { return defaultLineHeight + 4; } 11049 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 11050 override int maxWidth() { 11051 if(cast(MenuBar) parent) { 11052 return minWidth(); 11053 } 11054 return int.max; 11055 } 11056 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 11057 this(string lbl, Widget parent = null) { 11058 super(parent); 11059 //label = lbl; // FIXME 11060 foreach(char ch; lbl) // FIXME 11061 if(ch != '&') // FIXME 11062 label ~= ch; // FIXME 11063 tabStop = false; // these are selected some other way 11064 } 11065 11066 /// 11067 this(Action action, Widget parent = null) { 11068 assert(action !is null); 11069 this(action.label, parent); 11070 this.action = action; 11071 tabStop = false; // these are selected some other way 11072 } 11073 11074 version(custom_widgets) 11075 override void paint(WidgetPainter painter) { 11076 auto cs = getComputedStyle(); 11077 if(dynamicState & DynamicState.depressed) 11078 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11079 if(dynamicState & DynamicState.hover) 11080 painter.outlineColor = cs.activeMenuItemColor; 11081 else 11082 painter.outlineColor = cs.foregroundColor; 11083 painter.fillColor = Color.transparent; 11084 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11085 if(action && action.accelerator !is KeyEvent.init) { 11086 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 11087 11088 } 11089 } 11090 11091 static class Style : Widget.Style { 11092 override bool variesWithState(ulong dynamicStateFlags) { 11093 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11094 } 11095 } 11096 mixin OverrideStyle!Style; 11097 11098 override void defaultEventHandler_triggered(Event event) { 11099 if(action) 11100 foreach(handler; action.triggered) 11101 handler(); 11102 11103 if(auto pmenu = cast(Menu) this.parent) 11104 pmenu.remove(); 11105 11106 super.defaultEventHandler_triggered(event); 11107 } 11108 } 11109 11110 version(win32_widgets) 11111 /// A "mouse activiated widget" is really just an abstract variant of button. 11112 class MouseActivatedWidget : Widget { 11113 @property bool isChecked() { 11114 assert(hwnd); 11115 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 11116 11117 } 11118 @property void isChecked(bool state) { 11119 assert(hwnd); 11120 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 11121 11122 } 11123 11124 override void handleWmCommand(ushort cmd, ushort id) { 11125 if(cmd == 0) { 11126 auto event = new Event(EventType.triggered, this); 11127 event.dispatch(); 11128 } 11129 } 11130 11131 this(Widget parent) { 11132 super(parent); 11133 } 11134 } 11135 else version(custom_widgets) 11136 /// ditto 11137 class MouseActivatedWidget : Widget { 11138 @property bool isChecked() { return isChecked_; } 11139 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 11140 11141 private bool isChecked_; 11142 11143 this(Widget parent) { 11144 super(parent); 11145 11146 addEventListener((MouseDownEvent ev) { 11147 if(ev.button == MouseButton.left) { 11148 setDynamicState(DynamicState.depressed, true); 11149 setDynamicState(DynamicState.hover, true); 11150 redraw(); 11151 } 11152 }); 11153 11154 addEventListener((MouseUpEvent ev) { 11155 if(ev.button == MouseButton.left) { 11156 setDynamicState(DynamicState.depressed, false); 11157 setDynamicState(DynamicState.hover, false); 11158 redraw(); 11159 } 11160 }); 11161 11162 addEventListener((MouseMoveEvent mme) { 11163 if(!(mme.state & ModifierState.leftButtonDown)) { 11164 if(dynamicState_ & DynamicState.depressed) { 11165 setDynamicState(DynamicState.depressed, false); 11166 redraw(); 11167 } 11168 } 11169 }); 11170 } 11171 11172 override void defaultEventHandler_focus(Event ev) { 11173 super.defaultEventHandler_focus(ev); 11174 this.redraw(); 11175 } 11176 override void defaultEventHandler_blur(Event ev) { 11177 super.defaultEventHandler_blur(ev); 11178 setDynamicState(DynamicState.depressed, false); 11179 this.redraw(); 11180 } 11181 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11182 super.defaultEventHandler_keydown(ev); 11183 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 11184 setDynamicState(DynamicState.depressed, true); 11185 setDynamicState(DynamicState.hover, true); 11186 this.redraw(); 11187 } 11188 } 11189 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11190 super.defaultEventHandler_keyup(ev); 11191 if(!(dynamicState & DynamicState.depressed)) 11192 return; 11193 setDynamicState(DynamicState.depressed, false); 11194 setDynamicState(DynamicState.hover, false); 11195 this.redraw(); 11196 11197 auto event = new Event(EventType.triggered, this); 11198 event.sendDirectly(); 11199 } 11200 override void defaultEventHandler_click(ClickEvent ev) { 11201 super.defaultEventHandler_click(ev); 11202 if(ev.button == MouseButton.left) { 11203 auto event = new Event(EventType.triggered, this); 11204 event.sendDirectly(); 11205 } 11206 } 11207 11208 } 11209 else static assert(false); 11210 11211 /* 11212 /++ 11213 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11214 11215 Basically the same as a checkbox. 11216 +/ 11217 class OnOffSwitch : MouseActivatedWidget { 11218 11219 } 11220 */ 11221 11222 /++ 11223 History: 11224 Added June 15, 2021 (dub v10.1) 11225 +/ 11226 struct ImageLabel { 11227 /++ 11228 Defines a label+image combo used by some widgets. 11229 11230 If you provide just a text label, that is all the widget will try to 11231 display. Or just an image will display just that. If you provide both, 11232 it may display both text and image side by side or display the image 11233 and offer text on an input event depending on the widget. 11234 11235 History: 11236 The `alignment` parameter was added on September 27, 2021 11237 +/ 11238 this(string label, TextAlignment alignment = TextAlignment.Center) { 11239 this.label = label; 11240 this.displayFlags = DisplayFlags.displayText; 11241 this.alignment = alignment; 11242 } 11243 11244 /// ditto 11245 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11246 this.label = label; 11247 this.image = image; 11248 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11249 this.alignment = alignment; 11250 } 11251 11252 /// ditto 11253 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11254 this.image = image; 11255 this.displayFlags = DisplayFlags.displayImage; 11256 this.alignment = alignment; 11257 } 11258 11259 /// ditto 11260 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11261 this.label = label; 11262 this.image = image; 11263 this.alignment = alignment; 11264 this.displayFlags = displayFlags; 11265 } 11266 11267 string label; 11268 MemoryImage image; 11269 11270 enum DisplayFlags { 11271 displayText = 1 << 0, 11272 displayImage = 1 << 1, 11273 } 11274 11275 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11276 11277 TextAlignment alignment; 11278 } 11279 11280 /++ 11281 A basic checked or not checked box with an attached label. 11282 11283 11284 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 11285 11286 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11287 11288 History: 11289 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. 11290 +/ 11291 class Checkbox : MouseActivatedWidget { 11292 version(win32_widgets) { 11293 override int maxHeight() { return scaleWithDpi(16); } 11294 override int minHeight() { return scaleWithDpi(16); } 11295 } else version(custom_widgets) { 11296 private enum buttonSize = 16; 11297 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11298 override int minHeight() { return maxHeight(); } 11299 } else static assert(0); 11300 11301 override int marginLeft() { return 4; } 11302 11303 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11304 11305 /++ 11306 Just an alias because I keep typing checked out of web habit. 11307 11308 History: 11309 Added May 31, 2021 11310 +/ 11311 alias checked = isChecked; 11312 11313 private string label; 11314 private dchar accelerator; 11315 11316 /++ 11317 +/ 11318 this(string label, Widget parent) { 11319 this(ImageLabel(label), Appearance.checkbox, parent); 11320 } 11321 11322 /// ditto 11323 this(string label, Appearance appearance, Widget parent) { 11324 this(ImageLabel(label), appearance, parent); 11325 } 11326 11327 /++ 11328 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11329 11330 History: 11331 Added June 29, 2021 (dub v10.2) 11332 +/ 11333 enum Appearance { 11334 checkbox, /// a normal checkbox 11335 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11336 //sliderswitch, 11337 } 11338 private Appearance appearance; 11339 11340 /// ditto 11341 private this(ImageLabel label, Appearance appearance, Widget parent) { 11342 super(parent); 11343 version(win32_widgets) { 11344 this.label = label.label; 11345 11346 uint extraStyle; 11347 final switch(appearance) { 11348 case Appearance.checkbox: 11349 break; 11350 case Appearance.pushbutton: 11351 extraStyle |= BS_PUSHLIKE; 11352 break; 11353 } 11354 11355 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11356 } else version(custom_widgets) { 11357 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11358 } else static assert(0); 11359 } 11360 11361 version(custom_widgets) 11362 override void paint(WidgetPainter painter) { 11363 auto cs = getComputedStyle(); 11364 if(isFocused()) { 11365 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11366 painter.fillColor = cs.windowBackgroundColor; 11367 painter.drawRectangle(Point(0, 0), width, height); 11368 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11369 } else { 11370 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11371 painter.fillColor = cs.windowBackgroundColor; 11372 painter.drawRectangle(Point(0, 0), width, height); 11373 } 11374 11375 11376 painter.outlineColor = Color.black; 11377 painter.fillColor = Color.white; 11378 enum rectOffset = 2; 11379 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 11380 11381 if(isChecked) { 11382 auto size = scaleWithDpi(2); 11383 painter.pen = Pen(Color.black, size); 11384 // I'm using height so the checkbox is square 11385 enum padding = 3; 11386 painter.drawLine( 11387 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 11388 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 11389 ); 11390 painter.drawLine( 11391 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 11392 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 11393 ); 11394 11395 painter.pen = Pen(Color.black, 1); 11396 } 11397 11398 if(label !is null) { 11399 painter.outlineColor = cs.foregroundColor(); 11400 painter.fillColor = cs.foregroundColor(); 11401 11402 // i want the centerline of the text to be aligned with the centerline of the checkbox 11403 /+ 11404 auto font = cs.font(); 11405 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 11406 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 11407 +/ 11408 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 11409 } 11410 } 11411 11412 override void defaultEventHandler_triggered(Event ev) { 11413 isChecked = !isChecked; 11414 11415 this.emit!(ChangeEvent!bool)(&isChecked); 11416 11417 redraw(); 11418 } 11419 11420 /// Emits a change event with the checked state 11421 mixin Emits!(ChangeEvent!bool); 11422 } 11423 11424 /// Adds empty space to a layout. 11425 class VerticalSpacer : Widget { 11426 /// 11427 this(Widget parent) { 11428 super(parent); 11429 } 11430 } 11431 11432 /// ditto 11433 class HorizontalSpacer : Widget { 11434 /// 11435 this(Widget parent) { 11436 super(parent); 11437 this.tabStop = false; 11438 } 11439 } 11440 11441 11442 /++ 11443 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11444 11445 11446 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 11447 11448 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11449 11450 History: 11451 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. 11452 +/ 11453 class Radiobox : MouseActivatedWidget { 11454 11455 version(win32_widgets) { 11456 override int maxHeight() { return scaleWithDpi(16); } 11457 override int minHeight() { return scaleWithDpi(16); } 11458 } else version(custom_widgets) { 11459 private enum buttonSize = 16; 11460 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11461 override int minHeight() { return maxHeight(); } 11462 } else static assert(0); 11463 11464 override int marginLeft() { return 4; } 11465 11466 // FIXME: make a label getter 11467 private string label; 11468 private dchar accelerator; 11469 11470 /++ 11471 11472 +/ 11473 this(string label, Widget parent) { 11474 super(parent); 11475 version(win32_widgets) { 11476 this.label = label; 11477 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11478 } else version(custom_widgets) { 11479 label.extractWindowsStyleLabel(this.label, this.accelerator); 11480 height = 16; 11481 width = height + 4 + cast(int) label.length * 16; 11482 } 11483 } 11484 11485 version(custom_widgets) 11486 override void paint(WidgetPainter painter) { 11487 auto cs = getComputedStyle(); 11488 11489 if(isFocused) { 11490 painter.fillColor = cs.windowBackgroundColor; 11491 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11492 } else { 11493 painter.fillColor = cs.windowBackgroundColor; 11494 painter.outlineColor = cs.windowBackgroundColor; 11495 } 11496 painter.drawRectangle(Point(0, 0), width, height); 11497 11498 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11499 11500 painter.outlineColor = Color.black; 11501 painter.fillColor = Color.white; 11502 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11503 if(isChecked) { 11504 painter.outlineColor = Color.black; 11505 painter.fillColor = Color.black; 11506 // I'm using height so the checkbox is square 11507 auto size = scaleWithDpi(2); 11508 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 11509 } 11510 11511 painter.outlineColor = cs.foregroundColor(); 11512 painter.fillColor = cs.foregroundColor(); 11513 11514 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11515 } 11516 11517 11518 override void defaultEventHandler_triggered(Event ev) { 11519 isChecked = true; 11520 11521 if(this.parent) { 11522 foreach(child; this.parent.children) { 11523 if(child is this) continue; 11524 if(auto rb = cast(Radiobox) child) { 11525 rb.isChecked = false; 11526 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11527 rb.redraw(); 11528 } 11529 } 11530 } 11531 11532 this.emit!(ChangeEvent!bool)(&this.isChecked); 11533 11534 redraw(); 11535 } 11536 11537 /// 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. 11538 mixin Emits!(ChangeEvent!bool); 11539 } 11540 11541 11542 /++ 11543 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11544 11545 11546 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 11547 11548 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11549 11550 History: 11551 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. 11552 +/ 11553 class Button : MouseActivatedWidget { 11554 override int heightStretchiness() { return 3; } 11555 override int widthStretchiness() { return 3; } 11556 11557 /++ 11558 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. 11559 11560 History: 11561 Added July 2, 2021 11562 +/ 11563 public bool triggersOnMultiClick; 11564 11565 private string label_; 11566 private TextAlignment alignment; 11567 private dchar accelerator; 11568 11569 /// 11570 string label() { return label_; } 11571 /// 11572 void label(string l) { 11573 label_ = l; 11574 version(win32_widgets) { 11575 WCharzBuffer bfr = WCharzBuffer(l); 11576 SetWindowTextW(hwnd, bfr.ptr); 11577 } else version(custom_widgets) { 11578 redraw(); 11579 } 11580 } 11581 11582 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11583 super.defaultEventHandler_dblclick(ev); 11584 if(triggersOnMultiClick) { 11585 if(ev.button == MouseButton.left) { 11586 auto event = new Event(EventType.triggered, this); 11587 event.sendDirectly(); 11588 } 11589 } 11590 } 11591 11592 private Sprite sprite; 11593 private int displayFlags; 11594 11595 /++ 11596 Creates a push button with the given label, which may be an image or some text. 11597 11598 Bugs: 11599 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11600 11601 History: 11602 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11603 11604 The button with label and image will respect requests to show both on Windows as 11605 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11606 +/ 11607 this(ImageLabel label, Widget parent) { 11608 version(win32_widgets) { 11609 // FIXME: use ideal button size instead 11610 width = 50; 11611 height = 30; 11612 super(parent); 11613 11614 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11615 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11616 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11617 11618 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11619 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11620 11621 if(label.image) { 11622 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11623 11624 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11625 } 11626 11627 this.label = label.label; 11628 } else version(custom_widgets) { 11629 width = 50; 11630 height = 30; 11631 super(parent); 11632 11633 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11634 11635 if(label.image) { 11636 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11637 this.displayFlags = label.displayFlags; 11638 } 11639 11640 this.alignment = label.alignment; 11641 } 11642 } 11643 11644 /// 11645 this(string label, Widget parent) { 11646 this(ImageLabel(label), parent); 11647 } 11648 11649 override int minHeight() { return defaultLineHeight + 4; } 11650 11651 static class Style : Widget.Style { 11652 override WidgetBackground background() { 11653 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11654 11655 auto pressed = DynamicState.depressed | DynamicState.hover; 11656 if((widget.dynamicState & pressed) == pressed) { 11657 return WidgetBackground(cs.depressedButtonColor()); 11658 } else if(widget.dynamicState & DynamicState.hover) { 11659 return WidgetBackground(cs.hoveringColor()); 11660 } else { 11661 return WidgetBackground(cs.buttonColor()); 11662 } 11663 } 11664 11665 override FrameStyle borderStyle() { 11666 auto pressed = DynamicState.depressed | DynamicState.hover; 11667 if((widget.dynamicState & pressed) == pressed) { 11668 return FrameStyle.sunk; 11669 } else { 11670 return FrameStyle.risen; 11671 } 11672 11673 } 11674 11675 override bool variesWithState(ulong dynamicStateFlags) { 11676 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11677 } 11678 } 11679 mixin OverrideStyle!Style; 11680 11681 version(custom_widgets) 11682 override void paint(WidgetPainter painter) { 11683 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11684 if(sprite) { 11685 sprite.drawAt( 11686 painter, 11687 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11688 Point(0, 0) 11689 ); 11690 } else { 11691 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11692 } 11693 return bounds; 11694 }); 11695 } 11696 11697 override int flexBasisWidth() { 11698 version(win32_widgets) { 11699 SIZE size; 11700 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11701 if(size.cx == 0) 11702 goto fallback; 11703 return size.cx + scaleWithDpi(16); 11704 } 11705 fallback: 11706 return scaleWithDpi(cast(int) label.length * 8 + 16); 11707 } 11708 11709 override int flexBasisHeight() { 11710 version(win32_widgets) { 11711 SIZE size; 11712 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11713 if(size.cy == 0) 11714 goto fallback; 11715 return size.cy + scaleWithDpi(6); 11716 } 11717 fallback: 11718 return defaultLineHeight + 4; 11719 } 11720 } 11721 11722 /++ 11723 A button with a consistent size, suitable for user commands like OK and CANCEL. 11724 +/ 11725 class CommandButton : Button { 11726 this(string label, Widget parent) { 11727 super(label, parent); 11728 } 11729 11730 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11731 11732 override int maxHeight() { 11733 return defaultLineHeight + 4; 11734 } 11735 11736 override int maxWidth() { 11737 return defaultLineHeight * 4; 11738 } 11739 11740 override int marginLeft() { return 12; } 11741 override int marginRight() { return 12; } 11742 override int marginTop() { return 12; } 11743 override int marginBottom() { return 12; } 11744 } 11745 11746 /// 11747 enum ArrowDirection { 11748 left, /// 11749 right, /// 11750 up, /// 11751 down /// 11752 } 11753 11754 /// 11755 version(custom_widgets) 11756 class ArrowButton : Button { 11757 /// 11758 this(ArrowDirection direction, Widget parent) { 11759 super("", parent); 11760 this.direction = direction; 11761 triggersOnMultiClick = true; 11762 } 11763 11764 private ArrowDirection direction; 11765 11766 override int minHeight() { return scaleWithDpi(16); } 11767 override int maxHeight() { return scaleWithDpi(16); } 11768 override int minWidth() { return scaleWithDpi(16); } 11769 override int maxWidth() { return scaleWithDpi(16); } 11770 11771 override void paint(WidgetPainter painter) { 11772 super.paint(painter); 11773 11774 auto cs = getComputedStyle(); 11775 11776 painter.outlineColor = cs.foregroundColor; 11777 painter.fillColor = cs.foregroundColor; 11778 11779 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11780 11781 final switch(direction) { 11782 case ArrowDirection.up: 11783 painter.drawPolygon( 11784 scaleWithDpi(Point(2, 10) + offset), 11785 scaleWithDpi(Point(7, 5) + offset), 11786 scaleWithDpi(Point(12, 10) + offset), 11787 scaleWithDpi(Point(2, 10) + offset) 11788 ); 11789 break; 11790 case ArrowDirection.down: 11791 painter.drawPolygon( 11792 scaleWithDpi(Point(2, 6) + offset), 11793 scaleWithDpi(Point(7, 11) + offset), 11794 scaleWithDpi(Point(12, 6) + offset), 11795 scaleWithDpi(Point(2, 6) + offset) 11796 ); 11797 break; 11798 case ArrowDirection.left: 11799 painter.drawPolygon( 11800 scaleWithDpi(Point(10, 2) + offset), 11801 scaleWithDpi(Point(5, 7) + offset), 11802 scaleWithDpi(Point(10, 12) + offset), 11803 scaleWithDpi(Point(10, 2) + offset) 11804 ); 11805 break; 11806 case ArrowDirection.right: 11807 painter.drawPolygon( 11808 scaleWithDpi(Point(6, 2) + offset), 11809 scaleWithDpi(Point(11, 7) + offset), 11810 scaleWithDpi(Point(6, 12) + offset), 11811 scaleWithDpi(Point(6, 2) + offset) 11812 ); 11813 break; 11814 } 11815 } 11816 } 11817 11818 private 11819 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11820 int x, y; 11821 Widget par = c; 11822 while(par) { 11823 x += par.x; 11824 y += par.y; 11825 par = par.parent; 11826 } 11827 return [x, y]; 11828 } 11829 11830 version(win32_widgets) 11831 private 11832 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11833 // MapWindowPoints? 11834 int x, y; 11835 Widget par = c; 11836 while(par) { 11837 x += par.x; 11838 y += par.y; 11839 par = par.parent; 11840 if(par !is null && par.useNativeDrawing()) 11841 break; 11842 } 11843 return [x, y]; 11844 } 11845 11846 /// 11847 class ImageBox : Widget { 11848 private MemoryImage image_; 11849 11850 override int widthStretchiness() { return 1; } 11851 override int heightStretchiness() { return 1; } 11852 override int widthShrinkiness() { return 1; } 11853 override int heightShrinkiness() { return 1; } 11854 11855 override int flexBasisHeight() { 11856 return image_.height; 11857 } 11858 11859 override int flexBasisWidth() { 11860 return image_.width; 11861 } 11862 11863 /// 11864 public void setImage(MemoryImage image){ 11865 this.image_ = image; 11866 if(this.parentWindow && this.parentWindow.win) { 11867 if(sprite) 11868 sprite.dispose(); 11869 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11870 } 11871 redraw(); 11872 } 11873 11874 /// How to fit the image in the box if they aren't an exact match in size? 11875 enum HowToFit { 11876 center, /// centers the image, cropping around all the edges as needed 11877 crop, /// always draws the image in the upper left, cropping the lower right if needed 11878 // stretch, /// not implemented 11879 } 11880 11881 private Sprite sprite; 11882 private HowToFit howToFit_; 11883 11884 private Color backgroundColor_; 11885 11886 /// 11887 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11888 this.image_ = image; 11889 this.tabStop = false; 11890 this.howToFit_ = howToFit; 11891 this.backgroundColor_ = backgroundColor; 11892 super(parent); 11893 updateSprite(); 11894 } 11895 11896 /// ditto 11897 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11898 this(image, howToFit, Color.transparent, parent); 11899 } 11900 11901 private void updateSprite() { 11902 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11903 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11904 } 11905 } 11906 11907 override void paint(WidgetPainter painter) { 11908 updateSprite(); 11909 if(backgroundColor_.a) { 11910 painter.fillColor = backgroundColor_; 11911 painter.drawRectangle(Point(0, 0), width, height); 11912 } 11913 if(howToFit_ == HowToFit.crop) 11914 sprite.drawAt(painter, Point(0, 0)); 11915 else if(howToFit_ == HowToFit.center) { 11916 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11917 } 11918 } 11919 } 11920 11921 /// 11922 class TextLabel : Widget { 11923 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 11924 override int maxHeight() { return minHeight; } 11925 override int minWidth() { return 32; } 11926 11927 override int flexBasisHeight() { return minHeight(); } 11928 override int flexBasisWidth() { return defaultTextWidth(label); } 11929 11930 string label_; 11931 11932 /++ 11933 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11934 11935 In practice this means a click on the label will focus the `labelFor`. In future versions 11936 it will also set screen reader hints but that is not yet implemented. 11937 11938 History: 11939 Added October 3, 2021 (dub v10.4) 11940 +/ 11941 Widget labelFor; 11942 11943 /// 11944 @scriptable 11945 string label() { return label_; } 11946 11947 /// 11948 @scriptable 11949 void label(string l) { 11950 label_ = l; 11951 version(win32_widgets) { 11952 WCharzBuffer bfr = WCharzBuffer(l); 11953 SetWindowTextW(hwnd, bfr.ptr); 11954 } else version(custom_widgets) 11955 redraw(); 11956 } 11957 11958 override void defaultEventHandler_click(scope ClickEvent ce) { 11959 if(this.labelFor !is null) 11960 this.labelFor.focus(); 11961 } 11962 11963 /++ 11964 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11965 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11966 +/ 11967 this(string label, TextAlignment alignment, Widget parent) { 11968 this.label_ = label; 11969 this.alignment = alignment; 11970 this.tabStop = false; 11971 super(parent); 11972 11973 version(win32_widgets) 11974 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11975 } 11976 11977 /// ditto 11978 this(string label, Widget parent) { 11979 this(label, TextAlignment.Right, parent); 11980 } 11981 11982 TextAlignment alignment; 11983 11984 version(custom_widgets) 11985 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11986 painter.outlineColor = getComputedStyle().foregroundColor; 11987 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 11988 return bounds; 11989 } 11990 11991 } 11992 11993 version(custom_widgets) 11994 private struct etc { 11995 mixin ExperimentalTextComponent; 11996 } 11997 11998 version(win32_widgets) 11999 alias EditableTextWidgetParent = Widget; /// 12000 else version(custom_widgets) { 12001 version(trash_text) { 12002 alias EditableTextWidgetParent = ScrollableWidget; /// 12003 } else { 12004 alias EditableTextWidgetParent = Widget; 12005 version=use_new_text_system; 12006 import arsd.textlayouter; 12007 } 12008 } else static assert(0); 12009 12010 version(use_new_text_system) 12011 class TextDisplayHelper : Widget { 12012 protected TextLayouter l; 12013 protected ScrollMessageWidget smw; 12014 12015 private const(TextLayouter.State)*[] undoStack; 12016 private const(TextLayouter.State)*[] redoStack; 12017 12018 bool readonly; 12019 bool caretNavigation; // scroll lock can flip this 12020 bool singleLine; 12021 bool acceptsTabInput; 12022 12023 private Menu ctx; 12024 override Menu contextMenu(int x, int y) { 12025 if(ctx is null) { 12026 ctx = new Menu("Actions", this); 12027 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 12028 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 12029 ctx.addSeparator(); 12030 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 12031 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 12032 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 12033 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 12034 ctx.addSeparator(); 12035 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 12036 } 12037 return ctx; 12038 } 12039 12040 override void defaultEventHandler_blur(Event ev) { 12041 super.defaultEventHandler_blur(ev); 12042 if(l.wasMutated()) { 12043 auto evt = new ChangeEvent!string(this, &this.content); 12044 evt.dispatch(); 12045 l.clearWasMutatedFlag(); 12046 } 12047 } 12048 12049 private string content() { 12050 return l.getTextString(); 12051 } 12052 12053 void undo() { 12054 if(undoStack.length) { 12055 auto state = undoStack[$-1]; 12056 undoStack = undoStack[0 .. $-1]; 12057 undoStack.assumeSafeAppend(); 12058 redoStack ~= l.saveState(); 12059 l.restoreState(state); 12060 adjustScrollbarSizes(); 12061 scrollForCaret(); 12062 redraw(); 12063 stateCheckpoint = true; 12064 } 12065 } 12066 12067 void redo() { 12068 if(redoStack.length) { 12069 doStateCheckpoint(); 12070 auto state = redoStack[$-1]; 12071 redoStack = redoStack[0 .. $-1]; 12072 redoStack.assumeSafeAppend(); 12073 l.restoreState(state); 12074 adjustScrollbarSizes(); 12075 scrollForCaret(); 12076 redraw(); 12077 stateCheckpoint = true; 12078 } 12079 } 12080 12081 void cut() { 12082 with(l.selection()) { 12083 if(!isEmpty()) { 12084 setClipboardText(parentWindow.win, getContentString()); 12085 doStateCheckpoint(); 12086 replaceContent(""); 12087 adjustScrollbarSizes(); 12088 scrollForCaret(); 12089 this.redraw(); 12090 } 12091 } 12092 12093 } 12094 12095 void copy() { 12096 with(l.selection()) { 12097 if(!isEmpty()) { 12098 setClipboardText(parentWindow.win, getContentString()); 12099 this.redraw(); 12100 } 12101 } 12102 } 12103 12104 void paste() { 12105 getClipboardText(parentWindow.win, (txt) { 12106 doStateCheckpoint(); 12107 l.selection.replaceContent(txt); 12108 adjustScrollbarSizes(); 12109 scrollForCaret(); 12110 this.redraw(); 12111 }); 12112 } 12113 12114 void deleteContentOfSelection() { 12115 doStateCheckpoint(); 12116 l.selection.replaceContent(""); 12117 l.selection.setUserXCoordinate(); 12118 adjustScrollbarSizes(); 12119 scrollForCaret(); 12120 redraw(); 12121 } 12122 12123 void selectAll() { 12124 with(l.selection) { 12125 moveToStartOfDocument(); 12126 setAnchor(); 12127 moveToEndOfDocument(); 12128 setFocus(); 12129 } 12130 redraw(); 12131 } 12132 12133 protected bool stateCheckpoint = true; 12134 12135 protected void doStateCheckpoint() { 12136 if(stateCheckpoint) { 12137 undoStack ~= l.saveState(); 12138 stateCheckpoint = false; 12139 } 12140 } 12141 12142 protected void adjustScrollbarSizes() { 12143 // FIXME: will want a content area helper function instead of doing all these subtractions myself 12144 auto borderWidth = 2; 12145 this.smw.setTotalArea(l.width, l.height); 12146 this.smw.setViewableArea( 12147 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 12148 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 12149 } 12150 12151 protected void scrollForCaret() { 12152 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 12153 smw.scrollIntoView(l.selection.focusBoundingBox()); 12154 } 12155 12156 // FIXME: this should be a theme changed event listener instead 12157 private BaseVisualTheme currentTheme; 12158 override void recomputeChildLayout() { 12159 if(currentTheme is null) 12160 currentTheme = WidgetPainter.visualTheme; 12161 if(WidgetPainter.visualTheme !is currentTheme) { 12162 currentTheme = WidgetPainter.visualTheme; 12163 auto ds = this.l.defaultStyle; 12164 if(auto ms = cast(MyTextStyle) ds) { 12165 auto cs = getComputedStyle(); 12166 auto font = cs.font(); 12167 if(font !is null) 12168 ms.font_ = font; 12169 else { 12170 auto osc = new OperatingSystemFont(); 12171 osc.loadDefault; 12172 ms.font_ = osc; 12173 } 12174 } 12175 } 12176 super.recomputeChildLayout(); 12177 } 12178 12179 private Point adjustForSingleLine(Point p) { 12180 if(singleLine) 12181 return Point(p.x, this.height / 2); 12182 else 12183 return p; 12184 } 12185 12186 private bool wordWrapEnabled_; 12187 12188 this(TextLayouter l, ScrollMessageWidget parent) { 12189 this.smw = parent; 12190 12191 smw.addDefaultWheelListeners(16, 16, 8); 12192 smw.movementPerButtonClick(16, 16); 12193 12194 this.defaultPadding = Rectangle(2, 2, 2, 2); 12195 12196 this.l = l; 12197 super(parent); 12198 12199 smw.addEventListener((scope ScrollEvent se) { 12200 this.redraw(); 12201 }); 12202 12203 bool mouseDown; 12204 12205 this.addEventListener((scope ResizeEvent re) { 12206 // FIXME: I should add a method to give this client area width thing 12207 if(wordWrapEnabled_) 12208 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12209 12210 adjustScrollbarSizes(); 12211 scrollForCaret(); 12212 12213 this.redraw(); 12214 }); 12215 12216 this.addEventListener((scope KeyDownEvent kde) { 12217 switch(kde.key) { 12218 case Key.Up, Key.Down, Key.Left, Key.Right: 12219 case Key.Home, Key.End: 12220 stateCheckpoint = true; 12221 bool setPosition = false; 12222 switch(kde.key) { 12223 case Key.Up: l.selection.moveUp(); break; 12224 case Key.Down: l.selection.moveDown(); break; 12225 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12226 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12227 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12228 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12229 default: assert(0); 12230 } 12231 12232 if(kde.shiftKey) 12233 l.selection.setFocus(); 12234 else 12235 l.selection.setAnchor(); 12236 if(setPosition) 12237 l.selection.setUserXCoordinate(); 12238 scrollForCaret(); 12239 redraw(); 12240 break; 12241 case Key.PageUp, Key.PageDown: 12242 // FIXME 12243 scrollForCaret(); 12244 break; 12245 case Key.Delete: 12246 if(l.selection.isEmpty()) { 12247 l.selection.setAnchor(); 12248 l.selection.moveRight(); 12249 l.selection.setFocus(); 12250 } 12251 deleteContentOfSelection(); 12252 adjustScrollbarSizes(); 12253 scrollForCaret(); 12254 break; 12255 case Key.Insert: 12256 break; 12257 case Key.A: 12258 if(kde.ctrlKey) 12259 selectAll(); 12260 break; 12261 case Key.F: 12262 // find 12263 break; 12264 case Key.Z: 12265 if(kde.ctrlKey) 12266 undo(); 12267 break; 12268 case Key.R: 12269 if(kde.ctrlKey) 12270 redo(); 12271 break; 12272 case Key.X: 12273 if(kde.ctrlKey) 12274 cut(); 12275 break; 12276 case Key.C: 12277 if(kde.ctrlKey) 12278 copy(); 12279 break; 12280 case Key.V: 12281 if(kde.ctrlKey) 12282 paste(); 12283 break; 12284 case Key.F1: 12285 with(l.selection()) { 12286 moveToStartOfLine(); 12287 setAnchor(); 12288 moveToEndOfLine(); 12289 moveToIncludeAdjacentEndOfLineMarker(); 12290 setFocus(); 12291 replaceContent(""); 12292 } 12293 12294 redraw(); 12295 break; 12296 /* 12297 case Key.F2: 12298 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12299 //(cast(MyTextStyle) old).font, 12300 font2, 12301 Color.red))); 12302 redraw(); 12303 break; 12304 */ 12305 case Key.Tab: 12306 // we process the char event, so don't want to change focus on it 12307 if(acceptsTabInput) 12308 kde.preventDefault(); 12309 break; 12310 default: 12311 } 12312 }); 12313 12314 Point downAt; 12315 12316 static if(UsingSimpledisplayX11) 12317 this.addEventListener((scope ClickEvent ce) { 12318 if(ce.button == MouseButton.middle) { 12319 parentWindow.win.getPrimarySelection((txt) { 12320 l.selection.replaceContent(txt); 12321 redraw(); 12322 }); 12323 } 12324 }); 12325 12326 this.addEventListener((scope MouseDownEvent ce) { 12327 if(ce.button == MouseButton.left) { 12328 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12329 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12330 l.selection.setAnchor(); 12331 mouseDown = true; 12332 parentWindow.captureMouse(this); 12333 this.redraw(); 12334 } else if(ce.button == MouseButton.right) { 12335 this.showContextMenu(ce.clientX, ce.clientY); 12336 } 12337 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12338 }); 12339 12340 Timer autoscrollTimer; 12341 int autoscrollDirection; 12342 int autoscrollAmount; 12343 12344 void autoscroll() { 12345 switch(autoscrollDirection) { 12346 case 0: smw.scrollUp(autoscrollAmount); break; 12347 case 1: smw.scrollDown(autoscrollAmount); break; 12348 case 2: smw.scrollLeft(autoscrollAmount); break; 12349 case 3: smw.scrollRight(autoscrollAmount); break; 12350 default: assert(0); 12351 } 12352 12353 this.redraw(); 12354 } 12355 12356 void setAutoscrollTimer(int direction, int amount) { 12357 if(autoscrollTimer is null) { 12358 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12359 } 12360 12361 autoscrollDirection = direction; 12362 autoscrollAmount = amount; 12363 } 12364 12365 void stopAutoscrollTimer() { 12366 if(autoscrollTimer !is null) { 12367 autoscrollTimer.dispose(); 12368 autoscrollTimer = null; 12369 } 12370 autoscrollAmount = 0; 12371 autoscrollDirection = 0; 12372 } 12373 12374 this.addEventListener((scope MouseMoveEvent ce) { 12375 if(mouseDown) { 12376 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12377 12378 // FIXME: when scrolling i actually do want a timer. 12379 // i also want a zone near the sides of the window where i can auto scroll 12380 12381 auto scrollMultiplier = scaleWithDpi(16); 12382 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12383 12384 if(!singleLine && movedTo.y < 4) { 12385 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12386 } else 12387 if(!singleLine && (movedTo.y + 6) > this.height) { 12388 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12389 } else 12390 if(movedTo.x < 4) { 12391 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12392 } else 12393 if((movedTo.x + 6) > this.width) { 12394 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12395 } else 12396 stopAutoscrollTimer(); 12397 12398 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12399 l.selection.setFocus(); 12400 this.redraw(); 12401 } 12402 }); 12403 12404 this.addEventListener((scope MouseUpEvent ce) { 12405 // FIXME: assert primary selection 12406 if(mouseDown && ce.button == MouseButton.left) { 12407 stateCheckpoint = true; 12408 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12409 //l.selection.setFocus(); 12410 mouseDown = false; 12411 parentWindow.releaseMouseCapture(); 12412 stopAutoscrollTimer(); 12413 this.redraw(); 12414 } 12415 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12416 }); 12417 12418 this.addEventListener((scope CharEvent ce) { 12419 if(readonly) 12420 return; 12421 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12422 return; // skip the ctrl+x characters we don't care about as plain text 12423 12424 if(singleLine && ce.character == '\n') 12425 return; 12426 if(!acceptsTabInput && ce.character == '\t') 12427 return; 12428 12429 doStateCheckpoint(); 12430 12431 char[4] buffer; 12432 import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web 12433 auto stride = encode(buffer, ce.character); 12434 l.selection.replaceContent(buffer[0 .. stride]); 12435 l.selection.setUserXCoordinate(); 12436 adjustScrollbarSizes(); 12437 scrollForCaret(); 12438 redraw(); 12439 }); 12440 } 12441 12442 static class Style : Widget.Style { 12443 override WidgetBackground background() { 12444 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12445 } 12446 12447 override Color foregroundColor() { 12448 return WidgetPainter.visualTheme.foregroundColor; 12449 } 12450 12451 override FrameStyle borderStyle() { 12452 return FrameStyle.sunk; 12453 } 12454 12455 override MouseCursor cursor() { 12456 return GenericCursor.Text; 12457 } 12458 } 12459 mixin OverrideStyle!Style; 12460 12461 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 12462 override int maxHeight() { 12463 if(singleLine) 12464 return minHeight; 12465 else 12466 return super.maxHeight(); 12467 } 12468 12469 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12470 painter.drawText(upperLeft, text); 12471 } 12472 12473 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12474 //painter.setFont(font); 12475 12476 auto cs = getComputedStyle(); 12477 auto defaultColor = cs.foregroundColor; 12478 12479 auto old = painter.setClipRectangle(bounds); 12480 scope(exit) painter.setClipRectangle(old); 12481 12482 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12483 //writeln("Segment: ", txt); 12484 assert(style !is null); 12485 12486 auto myStyle = cast(MyTextStyle) style; 12487 assert(myStyle !is null); 12488 12489 painter.setFont(myStyle.font); 12490 // defaultColor = myStyle.color; // FIXME: so wrong 12491 12492 if(info.selections && info.boundingBox.width > 0) { 12493 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12494 painter.fillColor = color; 12495 painter.outlineColor = color; 12496 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12497 painter.outlineColor = cs.selectionForegroundColor; 12498 //painter.fillColor = Color.white; 12499 } else { 12500 painter.outlineColor = defaultColor; 12501 } 12502 12503 if(this.isFocused) 12504 foreach(idx, caret; carets) { 12505 if(idx == 0) 12506 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12507 painter.drawLine( 12508 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12509 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12510 ); 12511 } 12512 12513 if(txt.stripInternal.length) { 12514 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12515 } 12516 12517 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 12518 return false; 12519 } else { 12520 return true; 12521 } 12522 }, Rectangle(smw.position(), bounds.size)); 12523 12524 /+ 12525 int place = 0; 12526 int y = 75; 12527 foreach(width; widths) { 12528 painter.fillColor = Color.red; 12529 painter.drawRectangle(Point(place, y), Size(width, 75)); 12530 //y += 15; 12531 place += width; 12532 } 12533 +/ 12534 12535 return bounds; 12536 } 12537 12538 static class MyTextStyle : TextStyle { 12539 OperatingSystemFont font_; 12540 this(OperatingSystemFont font, bool passwordMode = false) { 12541 this.font_ = font; 12542 } 12543 12544 override OperatingSystemFont font() { 12545 return font_; 12546 } 12547 } 12548 } 12549 12550 /+ 12551 version(use_new_text_system) 12552 class TextWidget : Widget { 12553 TextLayouter l; 12554 ScrollMessageWidget smw; 12555 TextDisplayHelper helper; 12556 this(TextLayouter l, Widget parent) { 12557 this.l = l; 12558 super(parent); 12559 12560 smw = new ScrollMessageWidget(this); 12561 //smw.horizontalScrollBar.hide; 12562 //smw.verticalScrollBar.hide; 12563 smw.addDefaultWheelListeners(16, 16, 8); 12564 smw.movementPerButtonClick(16, 16); 12565 helper = new TextDisplayHelper(l, smw); 12566 12567 // no need to do this here since there's gonna be a resize 12568 // event immediately before any drawing 12569 // smw.setTotalArea(l.width, l.height); 12570 smw.setViewableArea( 12571 this.width - this.paddingLeft - this.paddingRight, 12572 this.height - this.paddingTop - this.paddingBottom); 12573 12574 /+ 12575 writeln(l.width, "x", l.height); 12576 +/ 12577 } 12578 } 12579 +/ 12580 12581 12582 12583 12584 /+ 12585 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12586 +/ 12587 12588 /// Contains the implementation of text editing 12589 abstract class EditableTextWidget : EditableTextWidgetParent { 12590 this(Widget parent) { 12591 super(parent); 12592 12593 version(custom_widgets) 12594 setupCustomTextEditing(); 12595 } 12596 12597 private bool wordWrapEnabled_; 12598 void wordWrapEnabled(bool enabled) { 12599 version(win32_widgets) { 12600 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12601 } else version(custom_widgets) { 12602 wordWrapEnabled_ = enabled; 12603 version(use_new_text_system) 12604 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12605 } else static assert(false); 12606 } 12607 12608 override int minWidth() { return scaleWithDpi(16); } 12609 override int widthStretchiness() { return 7; } 12610 12611 version(use_new_text_system) 12612 override int maxHeight() { return tdh.maxHeight; } 12613 12614 version(use_new_text_system) 12615 override void focus() { if(tdh) tdh.focus(); else super.focus(); } 12616 12617 void selectAll() { 12618 version(win32_widgets) 12619 SendMessage(hwnd, EM_SETSEL, 0, -1); 12620 else version(custom_widgets) { 12621 version(use_new_text_system) 12622 tdh.selectAll(); 12623 else 12624 textLayout.selectAll(); 12625 redraw(); 12626 } 12627 } 12628 12629 version(use_new_text_system) 12630 TextDisplayHelper tdh; 12631 12632 @property string content() { 12633 version(win32_widgets) { 12634 wchar[4096] bufferstack; 12635 wchar[] buffer; 12636 auto len = GetWindowTextLength(hwnd); 12637 if(len < bufferstack.length) 12638 buffer = bufferstack[0 .. len + 1]; 12639 else 12640 buffer = new wchar[](len + 1); 12641 12642 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12643 if(l >= 0) 12644 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12645 else 12646 return null; 12647 } else version(custom_widgets) { 12648 version(use_new_text_system) { 12649 return textLayout.getTextString(); 12650 } else 12651 return textLayout.getPlainText(); 12652 } else static assert(false); 12653 } 12654 @property void content(string s) { 12655 version(win32_widgets) { 12656 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12657 SetWindowTextW(hwnd, bfr.ptr); 12658 } else version(custom_widgets) { 12659 version(use_new_text_system) { 12660 selectAll(); 12661 textLayout.selection.replaceContent(s); 12662 12663 tdh.adjustScrollbarSizes(); 12664 // these don't seem to help 12665 // tdh.smw.setPosition(0, 0); 12666 // tdh.scrollForCaret(); 12667 12668 redraw(); 12669 } else { 12670 textLayout.clear(); 12671 textLayout.addText(s); 12672 12673 { 12674 // FIXME: it should be able to get this info easier 12675 auto painter = draw(); 12676 textLayout.redoLayout(painter); 12677 } 12678 auto cbb = textLayout.contentBoundingBox(); 12679 setContentSize(cbb.width, cbb.height); 12680 /* 12681 textLayout.addText(ForegroundColor.red, s); 12682 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12683 textLayout.addText(" is the best!"); 12684 */ 12685 redraw(); 12686 } 12687 } 12688 else static assert(false); 12689 } 12690 12691 void addText(string txt) { 12692 version(custom_widgets) { 12693 version(use_new_text_system) { 12694 textLayout.appendText(txt); 12695 tdh.adjustScrollbarSizes(); 12696 redraw(); 12697 } else { 12698 textLayout.addText(txt); 12699 12700 { 12701 // FIXME: it should be able to get this info easier 12702 auto painter = draw(); 12703 textLayout.redoLayout(painter); 12704 } 12705 auto cbb = textLayout.contentBoundingBox(); 12706 setContentSize(cbb.width, cbb.height); 12707 } 12708 } else version(win32_widgets) { 12709 // get the current selection 12710 DWORD StartPos, EndPos; 12711 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12712 12713 // move the caret to the end of the text 12714 int outLength = GetWindowTextLengthW(hwnd); 12715 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12716 12717 // insert the text at the new caret position 12718 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12719 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12720 12721 // restore the previous selection 12722 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12723 } else static assert(0); 12724 } 12725 12726 version(custom_widgets) 12727 version(trash_text) 12728 override void paintFrameAndBackground(WidgetPainter painter) { 12729 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12730 } 12731 12732 version(use_new_text_system) 12733 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12734 return new TextDisplayHelper(textLayout, smw); 12735 } 12736 12737 version(use_new_text_system) 12738 TextStyle defaultTextStyle() { 12739 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 12740 } 12741 12742 version(use_new_text_system) 12743 private OperatingSystemFont getUsedFont() { 12744 auto cs = getComputedStyle(); 12745 auto font = cs.font; 12746 if(font is null) { 12747 font = new OperatingSystemFont; 12748 font.loadDefault(); 12749 } 12750 return font; 12751 } 12752 12753 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 12754 else version(custom_widgets) { 12755 // FIXME 12756 version(use_new_text_system) { 12757 TextLayouter textLayout; 12758 12759 void setupCustomTextEditing() { 12760 textLayout = new TextLayouter(defaultTextStyle()); 12761 auto smw = new ScrollMessageWidget(this); 12762 if(!showingHorizontalScroll) 12763 smw.horizontalScrollBar.hide(); 12764 if(!showingVerticalScroll) 12765 smw.verticalScrollBar.hide(); 12766 this.tabStop = false; 12767 smw.tabStop = false; 12768 tdh = textDisplayHelperFactory(textLayout, smw); 12769 12770 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 12771 if(textLayout) { 12772 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 12773 // the dpi change can change the font, so this informs the layouter that it has changed too 12774 style.font_ = getUsedFont(); 12775 12776 // arsd.core.writeln(this.parentWindow.win.actualDpi); 12777 } 12778 } 12779 }); 12780 } 12781 12782 } else { 12783 12784 static if(SimpledisplayTimerAvailable) 12785 Timer caretTimer; 12786 etc.TextLayout textLayout; 12787 12788 void setupCustomTextEditing() { 12789 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12790 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12791 } 12792 12793 override void paint(WidgetPainter painter) { 12794 if(parentWindow.win.closed) return; 12795 12796 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12797 12798 /* 12799 painter.outlineColor = Color.white; 12800 painter.fillColor = Color.white; 12801 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12802 */ 12803 12804 painter.outlineColor = Color.black; 12805 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12806 12807 textLayout.caretShowingOnScreen = false; 12808 12809 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12810 } 12811 } 12812 12813 static class Style : Widget.Style { 12814 override FrameStyle borderStyle() { 12815 return FrameStyle.sunk; 12816 } 12817 override MouseCursor cursor() { 12818 return GenericCursor.Text; 12819 } 12820 } 12821 mixin OverrideStyle!Style; 12822 } 12823 else static assert(false); 12824 12825 version(trash_text) 12826 version(custom_widgets) 12827 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12828 super.defaultEventHandler_mousedown(ev); 12829 if(parentWindow.win.closed) return; 12830 if(ev.button == MouseButton.left) { 12831 if(textLayout.selectNone()) 12832 redraw(); 12833 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 12834 this.focus(); 12835 //this.parentWindow.win.grabInput(); 12836 } else if(ev.button == MouseButton.middle) { 12837 static if(UsingSimpledisplayX11) { 12838 getPrimarySelection(parentWindow.win, (in char[] txt) { 12839 textLayout.insert(txt); 12840 redraw(); 12841 12842 auto cbb = textLayout.contentBoundingBox(); 12843 setContentSize(cbb.width, cbb.height); 12844 }); 12845 } 12846 } 12847 } 12848 12849 version(trash_text) 12850 version(custom_widgets) 12851 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 12852 //this.parentWindow.win.releaseInputGrab(); 12853 super.defaultEventHandler_mouseup(ev); 12854 } 12855 12856 version(trash_text) 12857 version(custom_widgets) 12858 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 12859 super.defaultEventHandler_mousemove(ev); 12860 if(ev.state & ModifierState.leftButtonDown) { 12861 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 12862 redraw(); 12863 } 12864 } 12865 12866 version(trash_text) 12867 version(custom_widgets) 12868 override void defaultEventHandler_focus(Event ev) { 12869 super.defaultEventHandler_focus(ev); 12870 if(parentWindow.win.closed) return; 12871 auto painter = this.draw(); 12872 textLayout.drawCaret(painter); 12873 12874 static if(SimpledisplayTimerAvailable) 12875 if(caretTimer) { 12876 caretTimer.destroy(); 12877 caretTimer = null; 12878 } 12879 12880 bool blinkingCaret = true; 12881 static if(UsingSimpledisplayX11) 12882 if(!Image.impl.xshmAvailable) 12883 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 12884 12885 if(blinkingCaret) 12886 static if(SimpledisplayTimerAvailable) 12887 caretTimer = new Timer(500, { 12888 if(parentWindow.win.closed) { 12889 caretTimer.destroy(); 12890 return; 12891 } 12892 if(isFocused()) { 12893 auto painter = this.draw(); 12894 textLayout.drawCaret(painter); 12895 } else if(textLayout.caretShowingOnScreen) { 12896 auto painter = this.draw(); 12897 textLayout.eraseCaret(painter); 12898 } 12899 }); 12900 } 12901 12902 version(trash_text) { 12903 private string lastContentBlur; 12904 12905 override void defaultEventHandler_blur(Event ev) { 12906 super.defaultEventHandler_blur(ev); 12907 if(parentWindow.win.closed) return; 12908 version(custom_widgets) { 12909 auto painter = this.draw(); 12910 textLayout.eraseCaret(painter); 12911 static if(SimpledisplayTimerAvailable) 12912 if(caretTimer) { 12913 caretTimer.destroy(); 12914 caretTimer = null; 12915 } 12916 } 12917 12918 if(this.content != lastContentBlur) { 12919 auto evt = new ChangeEvent!string(this, &this.content); 12920 evt.dispatch(); 12921 lastContentBlur = this.content; 12922 } 12923 } 12924 } 12925 12926 version(win32_widgets) { 12927 private string lastContentBlur; 12928 12929 override void defaultEventHandler_blur(Event ev) { 12930 super.defaultEventHandler_blur(ev); 12931 12932 if(this.content != lastContentBlur) { 12933 auto evt = new ChangeEvent!string(this, &this.content); 12934 evt.dispatch(); 12935 lastContentBlur = this.content; 12936 } 12937 } 12938 } 12939 12940 12941 version(trash_text) 12942 version(custom_widgets) 12943 override void defaultEventHandler_char(CharEvent ev) { 12944 super.defaultEventHandler_char(ev); 12945 textLayout.insert(ev.character); 12946 redraw(); 12947 12948 // FIXME: too inefficient 12949 auto cbb = textLayout.contentBoundingBox(); 12950 setContentSize(cbb.width, cbb.height); 12951 } 12952 version(trash_text) 12953 version(custom_widgets) 12954 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12955 //super.defaultEventHandler_keydown(ev); 12956 switch(ev.key) { 12957 case Key.Delete: 12958 textLayout.delete_(); 12959 redraw(); 12960 break; 12961 case Key.Left: 12962 textLayout.moveLeft(); 12963 redraw(); 12964 break; 12965 case Key.Right: 12966 textLayout.moveRight(); 12967 redraw(); 12968 break; 12969 case Key.Up: 12970 textLayout.moveUp(); 12971 redraw(); 12972 break; 12973 case Key.Down: 12974 textLayout.moveDown(); 12975 redraw(); 12976 break; 12977 case Key.Home: 12978 textLayout.moveHome(); 12979 redraw(); 12980 break; 12981 case Key.End: 12982 textLayout.moveEnd(); 12983 redraw(); 12984 break; 12985 case Key.PageUp: 12986 foreach(i; 0 .. 32) 12987 textLayout.moveUp(); 12988 redraw(); 12989 break; 12990 case Key.PageDown: 12991 foreach(i; 0 .. 32) 12992 textLayout.moveDown(); 12993 redraw(); 12994 break; 12995 12996 default: 12997 {} // intentionally blank, let "char" handle it 12998 } 12999 /* 13000 if(ev.key == Key.Backspace) { 13001 textLayout.backspace(); 13002 redraw(); 13003 } 13004 */ 13005 ensureVisibleInScroll(textLayout.caretBoundingBox()); 13006 } 13007 13008 version(use_new_text_system) { 13009 bool showingVerticalScroll() { return true; } 13010 bool showingHorizontalScroll() { return true; } 13011 } 13012 } 13013 13014 /// 13015 class LineEdit : EditableTextWidget { 13016 // FIXME: hack 13017 version(custom_widgets) { 13018 override bool showingVerticalScroll() { return false; } 13019 override bool showingHorizontalScroll() { return false; } 13020 } 13021 13022 override int flexBasisWidth() { return 250; } 13023 13024 /// 13025 this(Widget parent) { 13026 super(parent); 13027 version(win32_widgets) { 13028 createWin32Window(this, "edit"w, "", 13029 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13030 } else version(custom_widgets) { 13031 version(trash_text) { 13032 setupCustomTextEditing(); 13033 addEventListener(delegate(CharEvent ev) { 13034 if(ev.character == '\n') 13035 ev.preventDefault(); 13036 }); 13037 } 13038 } else static assert(false); 13039 } 13040 13041 version(use_new_text_system) 13042 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13043 auto tdh = new TextDisplayHelper(textLayout, smw); 13044 tdh.singleLine = true; 13045 return tdh; 13046 } 13047 13048 version(win32_widgets) { 13049 mixin Padding!q{2}; 13050 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13051 override int maxHeight() { return minHeight; } 13052 } 13053 13054 /+ 13055 @property void passwordMode(bool p) { 13056 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 13057 } 13058 +/ 13059 } 13060 13061 /++ 13062 A [LineEdit] that displays `*` in place of the actual characters. 13063 13064 Alas, Windows requires the window to be created differently to use this style, 13065 so it had to be a new class instead of a toggle on and off on an existing object. 13066 13067 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 13068 13069 History: 13070 Added January 24, 2021 13071 +/ 13072 class PasswordEdit : EditableTextWidget { 13073 version(custom_widgets) { 13074 override bool showingVerticalScroll() { return false; } 13075 override bool showingHorizontalScroll() { return false; } 13076 } 13077 13078 override int flexBasisWidth() { return 250; } 13079 13080 version(use_new_text_system) 13081 override TextStyle defaultTextStyle() { 13082 auto cs = getComputedStyle(); 13083 13084 auto osf = new class OperatingSystemFont { 13085 this() { 13086 super(cs.font); 13087 } 13088 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 13089 int count = 0; 13090 foreach(dchar ch; text) 13091 count++; 13092 return count * super.stringWidth("*", window); 13093 } 13094 }; 13095 13096 return new TextDisplayHelper.MyTextStyle(osf); 13097 } 13098 13099 version(use_new_text_system) 13100 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13101 static class TDH : TextDisplayHelper { 13102 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13103 singleLine = true; 13104 super(textLayout, smw); 13105 } 13106 13107 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13108 char[256] buffer = void; 13109 int bufferLength = 0; 13110 foreach(dchar ch; text) 13111 buffer[bufferLength++] = '*'; 13112 painter.drawText(upperLeft, buffer[0..bufferLength]); 13113 } 13114 } 13115 13116 return new TDH(textLayout, smw); 13117 } 13118 13119 /// 13120 this(Widget parent) { 13121 super(parent); 13122 version(win32_widgets) { 13123 createWin32Window(this, "edit"w, "", 13124 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13125 } else version(custom_widgets) { 13126 version(trash_text) 13127 setupCustomTextEditing(); 13128 addEventListener(delegate(CharEvent ev) { 13129 if(ev.character == '\n') 13130 ev.preventDefault(); 13131 }); 13132 } else static assert(false); 13133 } 13134 version(win32_widgets) { 13135 mixin Padding!q{2}; 13136 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13137 override int maxHeight() { return minHeight; } 13138 } 13139 } 13140 13141 /// 13142 class TextEdit : EditableTextWidget { 13143 /// 13144 this(Widget parent) { 13145 super(parent); 13146 version(win32_widgets) { 13147 createWin32Window(this, "edit"w, "", 13148 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 13149 } else version(custom_widgets) { 13150 version(trash_text) 13151 setupCustomTextEditing(); 13152 } else static assert(false); 13153 } 13154 override int maxHeight() { return int.max; } 13155 override int heightStretchiness() { return 7; } 13156 13157 override int flexBasisWidth() { return 250; } 13158 override int flexBasisHeight() { return 25; } 13159 } 13160 13161 13162 /+ 13163 /++ 13164 13165 +/ 13166 version(none) 13167 class RichTextDisplay : Widget { 13168 @property void content(string c) {} 13169 void appendContent(string c) {} 13170 } 13171 +/ 13172 13173 /++ 13174 A read-only text display 13175 13176 History: 13177 Added October 31, 2023 (dub v11.3) 13178 +/ 13179 class TextDisplay : EditableTextWidget { 13180 this(string text, Widget parent) { 13181 super(parent); 13182 this.content = text; 13183 } 13184 13185 override int maxHeight() { return int.max; } 13186 override int minHeight() { return 50; } 13187 override int heightStretchiness() { return 7; } 13188 13189 override int flexBasisWidth() { return 250; } 13190 override int flexBasisHeight() { return 50; } 13191 13192 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13193 return new MyTextDisplayHelper(textLayout, smw); 13194 } 13195 13196 override void registerMovement() { 13197 super.registerMovement(); 13198 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 13199 } 13200 13201 static class MyTextDisplayHelper : TextDisplayHelper { 13202 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13203 smw.verticalScrollBar.hide(); 13204 smw.horizontalScrollBar.hide(); 13205 super(textLayout, smw); 13206 this.readonly = true; 13207 } 13208 13209 class Style : Widget.Style { 13210 // just want the generic look for these 13211 } 13212 13213 mixin OverrideStyle!Style; 13214 } 13215 } 13216 13217 /// 13218 class MessageBox : Window { 13219 private string message; 13220 MessageBoxButton buttonPressed = MessageBoxButton.None; 13221 /// 13222 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13223 super(300, 100); 13224 13225 assert(buttons.length); 13226 assert(buttons.length == buttonIds.length); 13227 13228 this.message = message; 13229 13230 auto label = new TextDisplay(message, this); 13231 13232 auto hl = new HorizontalLayout(this); 13233 auto spacer = new HorizontalSpacer(hl); // to right align 13234 13235 foreach(idx, buttonText; buttons) { 13236 auto button = new CommandButton(buttonText, hl); 13237 13238 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 13239 this.buttonPressed = buttonIds[idx]; 13240 win.close(); 13241 }; })(idx)); 13242 13243 if(idx == 0) 13244 button.focus(); 13245 } 13246 13247 if(buttons.length == 1) 13248 auto spacer2 = new HorizontalSpacer(hl); // to center it 13249 13250 win.resize(scaleWithDpi(300), this.minHeight()); 13251 13252 win.show(); 13253 redraw(); 13254 } 13255 13256 mixin Padding!q{16}; 13257 } 13258 13259 /// 13260 enum MessageBoxStyle { 13261 OK, /// 13262 OKCancel, /// 13263 RetryCancel, /// 13264 YesNo, /// 13265 YesNoCancel, /// 13266 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. 13267 } 13268 13269 /// 13270 enum MessageBoxIcon { 13271 None, /// 13272 Info, /// 13273 Warning, /// 13274 Error /// 13275 } 13276 13277 /// Identifies the button the user pressed on a message box. 13278 enum MessageBoxButton { 13279 None, /// The user closed the message box without clicking any of the buttons. 13280 OK, /// 13281 Cancel, /// 13282 Retry, /// 13283 Yes, /// 13284 No, /// 13285 Continue /// 13286 } 13287 13288 13289 /++ 13290 Displays a modal message box, blocking until the user dismisses it. 13291 13292 Returns: the button pressed. 13293 +/ 13294 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13295 version(win32_widgets) { 13296 WCharzBuffer t = WCharzBuffer(title); 13297 WCharzBuffer m = WCharzBuffer(message); 13298 UINT type; 13299 with(MessageBoxStyle) 13300 final switch(style) { 13301 case OK: type |= MB_OK; break; 13302 case OKCancel: type |= MB_OKCANCEL; break; 13303 case RetryCancel: type |= MB_RETRYCANCEL; break; 13304 case YesNo: type |= MB_YESNO; break; 13305 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13306 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13307 } 13308 with(MessageBoxIcon) 13309 final switch(icon) { 13310 case None: break; 13311 case Info: type |= MB_ICONINFORMATION; break; 13312 case Warning: type |= MB_ICONWARNING; break; 13313 case Error: type |= MB_ICONERROR; break; 13314 } 13315 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13316 case IDOK: return MessageBoxButton.OK; 13317 case IDCANCEL: return MessageBoxButton.Cancel; 13318 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13319 case IDYES: return MessageBoxButton.Yes; 13320 case IDNO: return MessageBoxButton.No; 13321 case IDCONTINUE: return MessageBoxButton.Continue; 13322 default: return MessageBoxButton.None; 13323 } 13324 } else { 13325 string[] buttons; 13326 MessageBoxButton[] buttonIds; 13327 with(MessageBoxStyle) 13328 final switch(style) { 13329 case OK: 13330 buttons = ["OK"]; 13331 buttonIds = [MessageBoxButton.OK]; 13332 break; 13333 case OKCancel: 13334 buttons = ["OK", "Cancel"]; 13335 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13336 break; 13337 case RetryCancel: 13338 buttons = ["Retry", "Cancel"]; 13339 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13340 break; 13341 case YesNo: 13342 buttons = ["Yes", "No"]; 13343 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13344 break; 13345 case YesNoCancel: 13346 buttons = ["Yes", "No", "Cancel"]; 13347 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13348 break; 13349 case RetryCancelContinue: 13350 buttons = ["Try Again", "Cancel", "Continue"]; 13351 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13352 break; 13353 } 13354 auto mb = new MessageBox(message, buttons, buttonIds); 13355 EventLoop el = EventLoop.get; 13356 el.run(() { return !mb.win.closed; }); 13357 return mb.buttonPressed; 13358 } 13359 } 13360 13361 /// ditto 13362 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13363 return messageBox(null, message, style, icon); 13364 } 13365 13366 13367 13368 /// 13369 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13370 13371 /++ 13372 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13373 13374 History: 13375 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. 13376 +/ 13377 struct EventListener { 13378 private Widget widget; 13379 private string event; 13380 private EventHandler handler; 13381 private bool useCapture; 13382 13383 /// 13384 void disconnect() { 13385 widget.removeEventListener(this); 13386 } 13387 } 13388 13389 /++ 13390 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13391 13392 Now, I recommend you use a statically typed event object instead. 13393 13394 See_Also: [Event] 13395 +/ 13396 enum EventType : string { 13397 click = "click", /// 13398 13399 mouseenter = "mouseenter", /// 13400 mouseleave = "mouseleave", /// 13401 mousein = "mousein", /// 13402 mouseout = "mouseout", /// 13403 mouseup = "mouseup", /// 13404 mousedown = "mousedown", /// 13405 mousemove = "mousemove", /// 13406 13407 keydown = "keydown", /// 13408 keyup = "keyup", /// 13409 char_ = "char", /// 13410 13411 focus = "focus", /// 13412 blur = "blur", /// 13413 13414 triggered = "triggered", /// 13415 13416 change = "change", /// 13417 } 13418 13419 /++ 13420 Represents an event that is currently being processed. 13421 13422 13423 Minigui's event model is based on the web browser. An event has a name, a target, 13424 and an associated data object. It starts from the window and works its way down through 13425 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13426 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13427 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13428 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13429 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13430 whenever propagation is done, not only if it gets to the end of the chain). 13431 13432 This model has several nice points: 13433 13434 $(LIST 13435 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13436 with event handlers set, then add/remove children as much as you want without needing 13437 to manage the event handlers on them - the parent alone can manage everything. 13438 13439 * It is easy to create new custom events in your application. 13440 13441 * It is familiar to many web developers. 13442 ) 13443 13444 There's a few downsides though: 13445 13446 $(LIST 13447 * There's not a lot of type safety. 13448 13449 * You don't get a static list of what events a widget can emit. 13450 13451 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13452 the central delegation benefit is it can be lead to debugging of action at a distance. 13453 ) 13454 13455 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13456 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13457 to simply use a D object type which provides a static interface as well as a built-in event name. 13458 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13459 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13460 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13461 to having a little more help from the D compiler and documentation generator. 13462 13463 Your code would change like this: 13464 13465 --- 13466 // old 13467 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13468 13469 // new 13470 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13471 --- 13472 13473 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. 13474 13475 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13476 13477 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13478 13479 Thus the family of functions are: 13480 13481 [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. 13482 13483 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13484 13485 [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. 13486 13487 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13488 13489 --- 13490 class MyCheckbox : Widget { 13491 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13492 /// It is NOT actually required but should be used whenever possible. 13493 mixin Emits!(ChangeEvent!bool); 13494 13495 this(Widget parent) { 13496 super(parent); 13497 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13498 } 13499 13500 private bool _checked; 13501 @property bool checked() { return _checked; } 13502 @property void checked(bool set) { 13503 _checked = set; 13504 emit!(ChangeEvent!bool)(&checked); 13505 } 13506 } 13507 --- 13508 13509 ## Creating Your Own Events 13510 13511 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. 13512 13513 --- 13514 class MyEvent : Event { 13515 this(Widget target) { super(EventString, target); } 13516 mixin Register; // adds EventString and other reflection information 13517 } 13518 --- 13519 13520 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13521 13522 History: 13523 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. 13524 13525 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. 13526 +/ 13527 /+ 13528 13529 ## General Conventions 13530 13531 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. 13532 13533 13534 ## Qt-style signals and slots 13535 13536 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. 13537 13538 The intention is for events to be used when 13539 13540 --- 13541 class Demo : Widget { 13542 this() { 13543 myPropertyChanged = Signal!int(this); 13544 } 13545 @property myProperty(int v) { 13546 myPropertyChanged.emit(v); 13547 } 13548 13549 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 13550 // but it can just genuinely not care about `this` since that's not really passed. 13551 } 13552 13553 class Foo : Widget { 13554 // the slot uda is not necessary, but it helps the script and ui builder find it. 13555 @slot void setValue(int v) { ... } 13556 } 13557 13558 demo.myPropertyChanged.connect(&foo.setValue); 13559 --- 13560 13561 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 13562 13563 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 13564 13565 class StringChangeEvent : ChangeEvent, Signal!string { 13566 mixin SignalImpl 13567 } 13568 13569 +/ 13570 class Event : ReflectableProperties { 13571 /// Creates an event without populating any members and without sending it. See [dispatch] 13572 this(string eventName, Widget emittedBy) { 13573 this.eventName = eventName; 13574 this.srcElement = emittedBy; 13575 } 13576 13577 13578 /// Implementations for the [ReflectableProperties] interface/ 13579 void getPropertiesList(scope void delegate(string name) sink) const {} 13580 /// ditto 13581 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 13582 /// ditto 13583 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 13584 return SetPropertyResult.notPermitted; 13585 } 13586 13587 13588 /+ 13589 /++ 13590 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 13591 13592 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. 13593 +/ 13594 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 13595 if(value.length == 0) { 13596 finalSink(memberName, `""`); 13597 return; 13598 } 13599 13600 char[1024] bufferBacking; 13601 char[] buffer = bufferBacking; 13602 int bufferPosition; 13603 13604 void sink(char ch) { 13605 if(bufferPosition >= buffer.length) 13606 buffer.length = buffer.length + 1024; 13607 buffer[bufferPosition++] = ch; 13608 } 13609 13610 sink('"'); 13611 13612 foreach(ch; value) { 13613 switch(ch) { 13614 case '\\': 13615 sink('\\'); sink('\\'); 13616 break; 13617 case '"': 13618 sink('\\'); sink('"'); 13619 break; 13620 case '\n': 13621 sink('\\'); sink('n'); 13622 break; 13623 case '\r': 13624 sink('\\'); sink('r'); 13625 break; 13626 case '\t': 13627 sink('\\'); sink('t'); 13628 break; 13629 default: 13630 sink(ch); 13631 } 13632 } 13633 13634 sink('"'); 13635 13636 finalSink(memberName, buffer[0 .. bufferPosition]); 13637 } 13638 +/ 13639 13640 /+ 13641 enum EventInitiator { 13642 system, 13643 minigui, 13644 user 13645 } 13646 13647 immutable EventInitiator; initiatedBy; 13648 +/ 13649 13650 /++ 13651 Events should generally follow the propagation model, but there's some exceptions 13652 to that rule. If so, they should override this to return false. In that case, only 13653 bubbling event handlers on the target itself and capturing event handlers on the containing 13654 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 13655 capture -> target -> bubble process.) 13656 13657 History: 13658 Added May 12, 2021 13659 +/ 13660 bool propagates() const pure nothrow @nogc @safe { 13661 return true; 13662 } 13663 13664 /++ 13665 hints as to whether preventDefault will actually do anything. not entirely reliable. 13666 13667 History: 13668 Added May 14, 2021 13669 +/ 13670 bool cancelable() const pure nothrow @nogc @safe { 13671 return true; 13672 } 13673 13674 /++ 13675 You can mix this into child class to register some boilerplate. It includes the `EventString` 13676 member, a constructor, and implementations of the dynamic get data interfaces. 13677 13678 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 13679 13680 13681 You can override the default EventString by simply providing your own in the form of 13682 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 13683 which provides some namespace protection against conflicts in other libraries while still being fairly 13684 easy to use. 13685 13686 If you provide your own constructor, it will override the default constructor provided here. A constructor 13687 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 13688 first argument to your constructor. 13689 13690 History: 13691 Added May 13, 2021. 13692 +/ 13693 protected static mixin template Register() { 13694 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 13695 this(Widget target) { super(EventString, target); } 13696 13697 mixin ReflectableProperties.RegisterGetters; 13698 } 13699 13700 /++ 13701 This is the widget that emitted the event. 13702 13703 13704 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 13705 13706 History: 13707 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 13708 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 13709 so I don't intend to remove these aliases. 13710 +/ 13711 Widget source; 13712 /// ditto 13713 alias source target; 13714 /// ditto 13715 alias source srcElement; 13716 13717 Widget relatedTarget; /// Note: likely to be deprecated at some point. 13718 13719 /// Prevents the default event handler (if there is one) from being called 13720 void preventDefault() { 13721 lastDefaultPrevented = true; 13722 defaultPrevented = true; 13723 } 13724 13725 /// Stops the event propagation immediately. 13726 void stopPropagation() { 13727 propagationStopped = true; 13728 } 13729 13730 private bool defaultPrevented; 13731 private bool propagationStopped; 13732 private string eventName; 13733 13734 private bool isBubbling; 13735 13736 /// 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. 13737 protected void adjustScrolling() { } 13738 /// ditto 13739 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 13740 13741 /++ 13742 this sends it only to the target. If you want propagation, use dispatch() instead. 13743 13744 This should be made private!!! 13745 13746 +/ 13747 void sendDirectly() { 13748 if(srcElement is null) 13749 return; 13750 13751 // 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. 13752 13753 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13754 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 13755 13756 adjustScrolling(); 13757 13758 if(auto e = target.parentWindow) { 13759 if(auto handlers = "*" in e.capturingEventHandlers) 13760 foreach(handler; *handlers) 13761 if(handler) handler(e, this); 13762 if(auto handlers = eventName in e.capturingEventHandlers) 13763 foreach(handler; *handlers) 13764 if(handler) handler(e, this); 13765 } 13766 13767 auto e = srcElement; 13768 13769 if(auto handlers = eventName in e.bubblingEventHandlers) 13770 foreach(handler; *handlers) 13771 if(handler) handler(e, this); 13772 13773 if(auto handlers = "*" in e.bubblingEventHandlers) 13774 foreach(handler; *handlers) 13775 if(handler) handler(e, this); 13776 13777 // there's never a default for a catch-all event 13778 if(!defaultPrevented) 13779 if(eventName in e.defaultEventHandlers) 13780 e.defaultEventHandlers[eventName](e, this); 13781 } 13782 13783 /// this dispatches the element using the capture -> target -> bubble process 13784 void dispatch() { 13785 if(srcElement is null) 13786 return; 13787 13788 if(!propagates) { 13789 sendDirectly; 13790 return; 13791 } 13792 13793 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13794 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 13795 13796 adjustScrolling(); 13797 // first capture, then bubble 13798 13799 Widget[] chain; 13800 Widget curr = srcElement; 13801 while(curr) { 13802 auto l = curr; 13803 chain ~= l; 13804 curr = curr.parent; 13805 } 13806 13807 isBubbling = false; 13808 13809 foreach_reverse(e; chain) { 13810 if(auto handlers = "*" in e.capturingEventHandlers) 13811 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13812 13813 if(propagationStopped) 13814 break; 13815 13816 if(auto handlers = eventName in e.capturingEventHandlers) 13817 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13818 13819 // the default on capture should really be to always do nothing 13820 13821 //if(!defaultPrevented) 13822 // if(eventName in e.defaultEventHandlers) 13823 // e.defaultEventHandlers[eventName](e.element, this); 13824 13825 if(propagationStopped) 13826 break; 13827 } 13828 13829 int adjustX; 13830 int adjustY; 13831 13832 isBubbling = true; 13833 if(!propagationStopped) 13834 foreach(e; chain) { 13835 if(auto handlers = eventName in e.bubblingEventHandlers) 13836 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13837 13838 if(propagationStopped) 13839 break; 13840 13841 if(auto handlers = "*" in e.bubblingEventHandlers) 13842 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13843 13844 if(propagationStopped) 13845 break; 13846 13847 if(e.encapsulatedChildren()) { 13848 adjustClientCoordinates(adjustX, adjustY); 13849 target = e; 13850 } else { 13851 adjustX += e.x; 13852 adjustY += e.y; 13853 } 13854 } 13855 13856 if(!defaultPrevented) 13857 foreach(e; chain) { 13858 if(eventName in e.defaultEventHandlers) 13859 e.defaultEventHandlers[eventName](e, this); 13860 } 13861 } 13862 13863 13864 /* old compatibility things */ 13865 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 13866 final @property { 13867 Key key() { return (cast(KeyEventBase) this).key; } 13868 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 13869 13870 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 13871 bool altKey() { return (cast(KeyEventBase) this).altKey; } 13872 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 13873 } 13874 13875 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 13876 final @property { 13877 int clientX() { return (cast(MouseEventBase) this).clientX; } 13878 int clientY() { return (cast(MouseEventBase) this).clientY; } 13879 13880 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 13881 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 13882 13883 int button() { return (cast(MouseEventBase) this).button; } 13884 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 13885 } 13886 13887 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 13888 final @property { 13889 int state() { 13890 if(auto meb = cast(MouseEventBase) this) 13891 return meb.state; 13892 if(auto keb = cast(KeyEventBase) this) 13893 return keb.state; 13894 assert(0); 13895 } 13896 } 13897 13898 deprecated("Use a CharEvent instead of Event in your handler going forward") 13899 final @property { 13900 dchar character() { 13901 if(auto ce = cast(CharEvent) this) 13902 return ce.character; 13903 return dchar.init; 13904 } 13905 } 13906 13907 // for change events 13908 @property { 13909 /// 13910 int intValue() { return 0; } 13911 /// 13912 string stringValue() { return null; } 13913 } 13914 } 13915 13916 /++ 13917 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 13918 13919 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 13920 dynamic and custom events, but the static list helps ensure you get them right. 13921 13922 If this is declared, you can use [Widget.emit] to send the event. 13923 13924 All events work the same way though, following the capture->widget->bubble model described under [Event]. 13925 13926 History: 13927 Added May 4, 2021 13928 +/ 13929 mixin template Emits(EventType) { 13930 import arsd.minigui : EventString; 13931 static if(is(EventType : Event) && !is(EventType == Event)) 13932 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 13933 else 13934 static assert(0, "You can only emit subclasses of Event"); 13935 } 13936 13937 /// ditto 13938 mixin template Emits(string eventString) { 13939 mixin("private Event[0] emits_" ~ eventString ~";"); 13940 } 13941 13942 /* 13943 class SignalEvent(string name) : Event { 13944 13945 } 13946 */ 13947 13948 /++ 13949 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". 13950 13951 13952 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. 13953 13954 History: 13955 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 13956 +/ 13957 class CommandEvent : Event { 13958 enum EventString = "command"; 13959 this(Widget source, string CommandString = EventString) { 13960 super(CommandString, source); 13961 } 13962 } 13963 13964 /++ 13965 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 13966 +/ 13967 class CommandEventWithArgs(Args...) : CommandEvent { 13968 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 13969 Args args; 13970 } 13971 13972 /++ 13973 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. 13974 13975 See [CommandEvent] for more information. 13976 13977 Returns: 13978 The [EventListener] you can use to remove the handler. 13979 +/ 13980 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 13981 return w.addEventListener(CommandString, (Event ev) { 13982 if(ev.target is w) 13983 return; // it does not consume its own commands! 13984 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 13985 handler(cev.args); 13986 ev.stopPropagation(); 13987 } 13988 }); 13989 } 13990 13991 /++ 13992 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. 13993 +/ 13994 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 13995 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 13996 event.dispatch(); 13997 } 13998 13999 class ResizeEvent : Event { 14000 enum EventString = "resize"; 14001 14002 this(Widget target) { super(EventString, target); } 14003 14004 override bool propagates() const { return false; } 14005 } 14006 14007 /++ 14008 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 14009 14010 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. 14011 14012 History: 14013 Added June 21, 2021 (dub v10.1) 14014 +/ 14015 class ClosingEvent : Event { 14016 enum EventString = "closing"; 14017 14018 this(Widget target) { super(EventString, target); } 14019 14020 override bool propagates() const { return false; } 14021 override bool cancelable() const { return true; } 14022 } 14023 14024 /// ditto 14025 class ClosedEvent : Event { 14026 enum EventString = "closed"; 14027 14028 this(Widget target) { super(EventString, target); } 14029 14030 override bool propagates() const { return false; } 14031 override bool cancelable() const { return false; } 14032 } 14033 14034 /// 14035 class BlurEvent : Event { 14036 enum EventString = "blur"; 14037 14038 // FIXME: related target? 14039 this(Widget target) { super(EventString, target); } 14040 14041 override bool propagates() const { return false; } 14042 } 14043 14044 /// 14045 class FocusEvent : Event { 14046 enum EventString = "focus"; 14047 14048 // FIXME: related target? 14049 this(Widget target) { super(EventString, target); } 14050 14051 override bool propagates() const { return false; } 14052 } 14053 14054 /++ 14055 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 14056 14057 History: 14058 Added July 3, 2021 14059 +/ 14060 class FocusInEvent : Event { 14061 enum EventString = "focusin"; 14062 14063 // FIXME: related target? 14064 this(Widget target) { super(EventString, target); } 14065 14066 override bool cancelable() const { return false; } 14067 } 14068 14069 /// ditto 14070 class FocusOutEvent : Event { 14071 enum EventString = "focusout"; 14072 14073 // FIXME: related target? 14074 this(Widget target) { super(EventString, target); } 14075 14076 override bool cancelable() const { return false; } 14077 } 14078 14079 /// 14080 class ScrollEvent : Event { 14081 enum EventString = "scroll"; 14082 this(Widget target) { super(EventString, target); } 14083 14084 override bool cancelable() const { return false; } 14085 } 14086 14087 /++ 14088 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 14089 14090 History: 14091 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 14092 +/ 14093 class CharEvent : Event { 14094 enum EventString = "char"; 14095 this(Widget target, dchar ch) { 14096 character = ch; 14097 super(EventString, target); 14098 } 14099 14100 immutable dchar character; 14101 } 14102 14103 /++ 14104 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 14105 +/ 14106 abstract class ChangeEventBase : Event { 14107 enum EventString = "change"; 14108 this(Widget target) { 14109 super(EventString, target); 14110 } 14111 14112 /+ 14113 // idk where or how exactly i want to do this. 14114 // i might come back to it later. 14115 14116 // If a widget itself broadcasts one of theses itself, it stops propagation going down 14117 // this way the source doesn't get too confused (think of a nested scroll widget) 14118 // 14119 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 14120 // then you consume that command and change you scroll x position to whatever. then you do 14121 // some kind of change event that is broadcast back to the children and any horizontal scroll 14122 // listeners are now able to update, without having an explicit connection between them. 14123 void broadcastToChildren(string fieldName) { 14124 14125 } 14126 +/ 14127 } 14128 14129 /++ 14130 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. 14131 14132 14133 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). 14134 14135 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);` 14136 14137 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 14138 14139 History: 14140 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. 14141 +/ 14142 class ChangeEvent(T) : ChangeEventBase { 14143 this(Widget target, T delegate() getNewValue) { 14144 assert(getNewValue !is null); 14145 this.getNewValue = getNewValue; 14146 super(target); 14147 } 14148 14149 private T delegate() getNewValue; 14150 14151 /++ 14152 Gets the new value that just changed. 14153 +/ 14154 @property T value() { 14155 return getNewValue(); 14156 } 14157 14158 /// compatibility method for old generic Events 14159 static if(is(immutable T == immutable int)) 14160 override int intValue() { return value; } 14161 /// ditto 14162 static if(is(immutable T == immutable string)) 14163 override string stringValue() { return value; } 14164 } 14165 14166 /++ 14167 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 14168 14169 14170 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14171 14172 History: 14173 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14174 +/ 14175 abstract class KeyEventBase : Event { 14176 this(string name, Widget target) { 14177 super(name, target); 14178 } 14179 14180 // for key events 14181 Key key; /// 14182 14183 KeyEvent originalKeyEvent; 14184 14185 /++ 14186 Indicates the current state of the given keyboard modifier keys. 14187 14188 History: 14189 Added to events on April 15, 2020. 14190 +/ 14191 bool ctrlKey; 14192 14193 /// ditto 14194 bool altKey; 14195 14196 /// ditto 14197 bool shiftKey; 14198 14199 /++ 14200 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 14201 14202 See [arsd.simpledisplay.ModifierState] for other possible flags. 14203 +/ 14204 int state; 14205 14206 mixin Register; 14207 } 14208 14209 /++ 14210 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]. 14211 14212 14213 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14214 14215 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. 14216 14217 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. 14218 14219 See_Also: [KeyUpEvent], [CharEvent] 14220 14221 History: 14222 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 14223 +/ 14224 class KeyDownEvent : KeyEventBase { 14225 enum EventString = "keydown"; 14226 this(Widget target) { super(EventString, target); } 14227 } 14228 14229 /++ 14230 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 14231 14232 14233 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14234 14235 See_Also: [KeyDownEvent], [CharEvent] 14236 14237 History: 14238 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 14239 +/ 14240 class KeyUpEvent : KeyEventBase { 14241 enum EventString = "keyup"; 14242 this(Widget target) { super(EventString, target); } 14243 } 14244 14245 /++ 14246 Contains shared properties for various mouse events; 14247 14248 14249 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14250 14251 History: 14252 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14253 +/ 14254 abstract class MouseEventBase : Event { 14255 this(string name, Widget target) { 14256 super(name, target); 14257 } 14258 14259 // for mouse events 14260 int clientX; /// The mouse event location relative to the target widget 14261 int clientY; /// ditto 14262 14263 int viewportX; /// The mouse event location relative to the window origin 14264 int viewportY; /// ditto 14265 14266 int button; /// See: [MouseEvent.button] 14267 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14268 14269 /++ 14270 Indicates the current state of the given keyboard modifier keys. 14271 14272 History: 14273 Added to mouse events on September 28, 2010. 14274 +/ 14275 bool ctrlKey; 14276 14277 /// ditto 14278 bool altKey; 14279 14280 /// ditto 14281 bool shiftKey; 14282 14283 14284 14285 int state; /// 14286 14287 /++ 14288 for consistent names with key event. 14289 14290 History: 14291 Added September 28, 2021 (dub v10.3) 14292 +/ 14293 alias modifierState = state; 14294 14295 /++ 14296 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14297 14298 History: 14299 Added May 15, 2021 14300 +/ 14301 bool isMouseWheel() { 14302 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14303 } 14304 14305 // private 14306 override void adjustClientCoordinates(int deltaX, int deltaY) { 14307 clientX += deltaX; 14308 clientY += deltaY; 14309 } 14310 14311 override void adjustScrolling() { 14312 version(custom_widgets) { // TEMP 14313 viewportX = clientX; 14314 viewportY = clientY; 14315 if(auto se = cast(ScrollableWidget) srcElement) { 14316 clientX += se.scrollOrigin.x; 14317 clientY += se.scrollOrigin.y; 14318 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14319 //clientX += se.scrollX_; 14320 //clientY += se.scrollY_; 14321 } 14322 } 14323 } 14324 14325 mixin Register; 14326 } 14327 14328 /++ 14329 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14330 14331 14332 $(WARNING 14333 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14334 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14335 behavior. 14336 ) 14337 14338 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14339 14340 [MouseUpEvent] is sent when the user releases a mouse button. 14341 14342 [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.) 14343 14344 [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. 14345 14346 [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. 14347 14348 [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. 14349 14350 [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. 14351 14352 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14353 14354 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14355 14356 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14357 14358 Rationale: 14359 14360 If you only want to do drag, mousedown/up works just fine being consistently sent. 14361 14362 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). 14363 14364 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. 14365 14366 History: 14367 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. 14368 +/ 14369 class MouseUpEvent : MouseEventBase { 14370 enum EventString = "mouseup"; /// 14371 this(Widget target) { super(EventString, target); } 14372 } 14373 /// ditto 14374 class MouseDownEvent : MouseEventBase { 14375 enum EventString = "mousedown"; /// 14376 this(Widget target) { super(EventString, target); } 14377 } 14378 /// ditto 14379 class MouseMoveEvent : MouseEventBase { 14380 enum EventString = "mousemove"; /// 14381 this(Widget target) { super(EventString, target); } 14382 } 14383 /// ditto 14384 class ClickEvent : MouseEventBase { 14385 enum EventString = "click"; /// 14386 this(Widget target) { super(EventString, target); } 14387 } 14388 /// ditto 14389 class DoubleClickEvent : MouseEventBase { 14390 enum EventString = "dblclick"; /// 14391 this(Widget target) { super(EventString, target); } 14392 } 14393 /// ditto 14394 class MouseOverEvent : Event { 14395 enum EventString = "mouseover"; /// 14396 this(Widget target) { super(EventString, target); } 14397 } 14398 /// ditto 14399 class MouseOutEvent : Event { 14400 enum EventString = "mouseout"; /// 14401 this(Widget target) { super(EventString, target); } 14402 } 14403 /// ditto 14404 class MouseEnterEvent : Event { 14405 enum EventString = "mouseenter"; /// 14406 this(Widget target) { super(EventString, target); } 14407 14408 override bool propagates() const { return false; } 14409 } 14410 /// ditto 14411 class MouseLeaveEvent : Event { 14412 enum EventString = "mouseleave"; /// 14413 this(Widget target) { super(EventString, target); } 14414 14415 override bool propagates() const { return false; } 14416 } 14417 14418 private bool isAParentOf(Widget a, Widget b) { 14419 if(a is null || b is null) 14420 return false; 14421 14422 while(b !is null) { 14423 if(a is b) 14424 return true; 14425 b = b.parent; 14426 } 14427 14428 return false; 14429 } 14430 14431 private struct WidgetAtPointResponse { 14432 Widget widget; 14433 14434 // x, y relative to the widget in the response. 14435 int x; 14436 int y; 14437 } 14438 14439 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14440 assert(starting !is null); 14441 14442 starting.addScrollPosition(x, y); 14443 14444 auto child = starting.getChildAtPosition(x, y); 14445 while(child) { 14446 if(child.hidden) 14447 continue; 14448 starting = child; 14449 x -= child.x; 14450 y -= child.y; 14451 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14452 child = r.widget; 14453 if(child is starting) 14454 break; 14455 } 14456 return WidgetAtPointResponse(starting, x, y); 14457 } 14458 14459 version(win32_widgets) { 14460 private: 14461 import core.sys.windows.commctrl; 14462 14463 pragma(lib, "comctl32"); 14464 shared static this() { 14465 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14466 INITCOMMONCONTROLSEX ic; 14467 ic.dwSize = cast(DWORD) ic.sizeof; 14468 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14469 if(!InitCommonControlsEx(&ic)) { 14470 //writeln("ICC failed"); 14471 } 14472 } 14473 14474 14475 // everything from here is just win32 headers copy pasta 14476 private: 14477 extern(Windows): 14478 14479 alias HANDLE HMENU; 14480 HMENU CreateMenu(); 14481 bool SetMenu(HWND, HMENU); 14482 HMENU CreatePopupMenu(); 14483 enum MF_POPUP = 0x10; 14484 enum MF_STRING = 0; 14485 14486 14487 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14488 struct INITCOMMONCONTROLSEX { 14489 DWORD dwSize; 14490 DWORD dwICC; 14491 } 14492 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14493 enum { 14494 IDB_STD_SMALL_COLOR, 14495 IDB_STD_LARGE_COLOR, 14496 IDB_VIEW_SMALL_COLOR = 4, 14497 IDB_VIEW_LARGE_COLOR = 5 14498 } 14499 enum { 14500 STD_CUT, 14501 STD_COPY, 14502 STD_PASTE, 14503 STD_UNDO, 14504 STD_REDOW, 14505 STD_DELETE, 14506 STD_FILENEW, 14507 STD_FILEOPEN, 14508 STD_FILESAVE, 14509 STD_PRINTPRE, 14510 STD_PROPERTIES, 14511 STD_HELP, 14512 STD_FIND, 14513 STD_REPLACE, 14514 STD_PRINT // = 14 14515 } 14516 14517 alias HANDLE HIMAGELIST; 14518 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14519 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14520 BOOL ImageList_Destroy(HIMAGELIST); 14521 14522 uint MAKELONG(ushort a, ushort b) { 14523 return cast(uint) ((b << 16) | a); 14524 } 14525 14526 14527 struct TBBUTTON { 14528 int iBitmap; 14529 int idCommand; 14530 BYTE fsState; 14531 BYTE fsStyle; 14532 version(Win64) 14533 BYTE[6] bReserved; 14534 else 14535 BYTE[2] bReserved; 14536 DWORD dwData; 14537 INT_PTR iString; 14538 } 14539 14540 enum { 14541 TB_ADDBUTTONSA = WM_USER + 20, 14542 TB_INSERTBUTTONA = WM_USER + 21, 14543 TB_GETIDEALSIZE = WM_USER + 99, 14544 } 14545 14546 struct SIZE { 14547 LONG cx; 14548 LONG cy; 14549 } 14550 14551 14552 enum { 14553 TBSTATE_CHECKED = 1, 14554 TBSTATE_PRESSED = 2, 14555 TBSTATE_ENABLED = 4, 14556 TBSTATE_HIDDEN = 8, 14557 TBSTATE_INDETERMINATE = 16, 14558 TBSTATE_WRAP = 32 14559 } 14560 14561 14562 14563 enum { 14564 ILC_COLOR = 0, 14565 ILC_COLOR4 = 4, 14566 ILC_COLOR8 = 8, 14567 ILC_COLOR16 = 16, 14568 ILC_COLOR24 = 24, 14569 ILC_COLOR32 = 32, 14570 ILC_COLORDDB = 254, 14571 ILC_MASK = 1, 14572 ILC_PALETTE = 2048 14573 } 14574 14575 14576 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 14577 14578 14579 enum { 14580 TB_ENABLEBUTTON = WM_USER + 1, 14581 TB_CHECKBUTTON, 14582 TB_PRESSBUTTON, 14583 TB_HIDEBUTTON, 14584 TB_INDETERMINATE, // = WM_USER + 5, 14585 TB_ISBUTTONENABLED = WM_USER + 9, 14586 TB_ISBUTTONCHECKED, 14587 TB_ISBUTTONPRESSED, 14588 TB_ISBUTTONHIDDEN, 14589 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 14590 TB_SETSTATE = WM_USER + 17, 14591 TB_GETSTATE = WM_USER + 18, 14592 TB_ADDBITMAP = WM_USER + 19, 14593 TB_DELETEBUTTON = WM_USER + 22, 14594 TB_GETBUTTON, 14595 TB_BUTTONCOUNT, 14596 TB_COMMANDTOINDEX, 14597 TB_SAVERESTOREA, 14598 TB_CUSTOMIZE, 14599 TB_ADDSTRINGA, 14600 TB_GETITEMRECT, 14601 TB_BUTTONSTRUCTSIZE, 14602 TB_SETBUTTONSIZE, 14603 TB_SETBITMAPSIZE, 14604 TB_AUTOSIZE, // = WM_USER + 33, 14605 TB_GETTOOLTIPS = WM_USER + 35, 14606 TB_SETTOOLTIPS = WM_USER + 36, 14607 TB_SETPARENT = WM_USER + 37, 14608 TB_SETROWS = WM_USER + 39, 14609 TB_GETROWS, 14610 TB_GETBITMAPFLAGS, 14611 TB_SETCMDID, 14612 TB_CHANGEBITMAP, 14613 TB_GETBITMAP, 14614 TB_GETBUTTONTEXTA, 14615 TB_REPLACEBITMAP, // = WM_USER + 46, 14616 TB_GETBUTTONSIZE = WM_USER + 58, 14617 TB_SETBUTTONWIDTH = WM_USER + 59, 14618 TB_GETBUTTONTEXTW = WM_USER + 75, 14619 TB_SAVERESTOREW = WM_USER + 76, 14620 TB_ADDSTRINGW = WM_USER + 77, 14621 } 14622 14623 extern(Windows) 14624 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 14625 14626 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 14627 14628 14629 enum { 14630 TB_SETINDENT = WM_USER + 47, 14631 TB_SETIMAGELIST, 14632 TB_GETIMAGELIST, 14633 TB_LOADIMAGES, 14634 TB_GETRECT, 14635 TB_SETHOTIMAGELIST, 14636 TB_GETHOTIMAGELIST, 14637 TB_SETDISABLEDIMAGELIST, 14638 TB_GETDISABLEDIMAGELIST, 14639 TB_SETSTYLE, 14640 TB_GETSTYLE, 14641 //TB_GETBUTTONSIZE, 14642 //TB_SETBUTTONWIDTH, 14643 TB_SETMAXTEXTROWS, 14644 TB_GETTEXTROWS // = WM_USER + 61 14645 } 14646 14647 enum { 14648 CCM_FIRST = 0x2000, 14649 CCM_LAST = CCM_FIRST + 0x200, 14650 CCM_SETBKCOLOR = 8193, 14651 CCM_SETCOLORSCHEME = 8194, 14652 CCM_GETCOLORSCHEME = 8195, 14653 CCM_GETDROPTARGET = 8196, 14654 CCM_SETUNICODEFORMAT = 8197, 14655 CCM_GETUNICODEFORMAT = 8198, 14656 CCM_SETVERSION = 0x2007, 14657 CCM_GETVERSION = 0x2008, 14658 CCM_SETNOTIFYWINDOW = 0x2009 14659 } 14660 14661 14662 enum { 14663 PBM_SETRANGE = WM_USER + 1, 14664 PBM_SETPOS, 14665 PBM_DELTAPOS, 14666 PBM_SETSTEP, 14667 PBM_STEPIT, // = WM_USER + 5 14668 PBM_SETRANGE32 = 1030, 14669 PBM_GETRANGE, 14670 PBM_GETPOS, 14671 PBM_SETBARCOLOR, // = 1033 14672 PBM_SETBKCOLOR = CCM_SETBKCOLOR 14673 } 14674 14675 enum { 14676 PBS_SMOOTH = 1, 14677 PBS_VERTICAL = 4 14678 } 14679 14680 enum { 14681 ICC_LISTVIEW_CLASSES = 1, 14682 ICC_TREEVIEW_CLASSES = 2, 14683 ICC_BAR_CLASSES = 4, 14684 ICC_TAB_CLASSES = 8, 14685 ICC_UPDOWN_CLASS = 16, 14686 ICC_PROGRESS_CLASS = 32, 14687 ICC_HOTKEY_CLASS = 64, 14688 ICC_ANIMATE_CLASS = 128, 14689 ICC_WIN95_CLASSES = 255, 14690 ICC_DATE_CLASSES = 256, 14691 ICC_USEREX_CLASSES = 512, 14692 ICC_COOL_CLASSES = 1024, 14693 ICC_STANDARD_CLASSES = 0x00004000, 14694 } 14695 14696 enum WM_USER = 1024; 14697 } 14698 14699 version(win32_widgets) 14700 pragma(lib, "comdlg32"); 14701 14702 14703 /// 14704 enum GenericIcons : ushort { 14705 None, /// 14706 // these happen to match the win32 std icons numerically if you just subtract one from the value 14707 Cut, /// 14708 Copy, /// 14709 Paste, /// 14710 Undo, /// 14711 Redo, /// 14712 Delete, /// 14713 New, /// 14714 Open, /// 14715 Save, /// 14716 PrintPreview, /// 14717 Properties, /// 14718 Help, /// 14719 Find, /// 14720 Replace, /// 14721 Print, /// 14722 } 14723 14724 enum FileDialogType { 14725 Automatic, 14726 Open, 14727 Save 14728 } 14729 string previousFileReferenced; 14730 14731 /++ 14732 Used in automatic menu functions to indicate that the user should be able to browse for a file. 14733 14734 Params: 14735 storage = an alias to a `static string` variable that stores the last file referenced. It will 14736 use this to pre-fill the dialog with a suggestion. 14737 14738 Please note that it MUST be `static` or you will get compile errors. 14739 14740 filters = the filters param to [getFileName] 14741 14742 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 14743 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 14744 a save dialog box. Otherwise, it will show an open dialog box. 14745 +/ 14746 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 14747 string name; 14748 alias name this; 14749 } 14750 14751 /++ 14752 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. 14753 14754 History: 14755 onCancel was added November 6, 2021. 14756 14757 The dialog itself on Linux was modified on December 2, 2021 to include 14758 a directory picker in addition to the command line completion view. 14759 14760 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 14761 Future_directions: 14762 I want to add some kind of custom preview and maybe thumbnail thing in the future, 14763 at least on Linux, maybe on Windows too. 14764 +/ 14765 void getOpenFileName( 14766 void delegate(string) onOK, 14767 string prefilledName = null, 14768 string[] filters = null, 14769 void delegate() onCancel = null, 14770 string initialDirectory = null, 14771 ) 14772 { 14773 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 14774 } 14775 14776 /// ditto 14777 void getSaveFileName( 14778 void delegate(string) onOK, 14779 string prefilledName = null, 14780 string[] filters = null, 14781 void delegate() onCancel = null, 14782 string initialDirectory = null, 14783 ) 14784 { 14785 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 14786 } 14787 14788 void getFileName( 14789 bool openOrSave, 14790 void delegate(string) onOK, 14791 string prefilledName = null, 14792 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 14793 void delegate() onCancel = null, 14794 string initialDirectory = null, 14795 ) 14796 { 14797 14798 version(win32_widgets) { 14799 import core.sys.windows.commdlg; 14800 /* 14801 Ofn.lStructSize = sizeof(OPENFILENAME); 14802 Ofn.hwndOwner = hWnd; 14803 Ofn.lpstrFilter = szFilter; 14804 Ofn.lpstrFile= szFile; 14805 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 14806 Ofn.lpstrFileTitle = szFileTitle; 14807 Ofn.nMaxFileTitle = sizeof(szFileTitle); 14808 Ofn.lpstrInitialDir = (LPSTR)NULL; 14809 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 14810 Ofn.lpstrTitle = szTitle; 14811 */ 14812 14813 14814 wchar[1024] file = 0; 14815 wchar[1024] filterBuffer = 0; 14816 makeWindowsString(prefilledName, file[]); 14817 OPENFILENAME ofn; 14818 ofn.lStructSize = ofn.sizeof; 14819 if(filters.length) { 14820 string filter; 14821 foreach(i, f; filters) { 14822 filter ~= f; 14823 filter ~= "\0"; 14824 } 14825 filter ~= "\0"; 14826 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 14827 } 14828 ofn.lpstrFile = file.ptr; 14829 ofn.nMaxFile = file.length; 14830 14831 wchar[1024] initialDir = 0; 14832 if(initialDirectory !is null) { 14833 makeWindowsString(initialDirectory, initialDir[]); 14834 ofn.lpstrInitialDir = file.ptr; 14835 } 14836 14837 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 14838 { 14839 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 14840 if(okString.length && okString[$-1] == '\0') 14841 okString = okString[0..$-1]; 14842 onOK(okString); 14843 } else { 14844 if(onCancel) 14845 onCancel(); 14846 } 14847 } else version(custom_widgets) { 14848 if(filters.length == 0) 14849 filters = ["All Files\0*.*"]; 14850 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 14851 picker.onOK = onOK; 14852 picker.onCancel = onCancel; 14853 picker.show(); 14854 } 14855 } 14856 14857 version(custom_widgets) 14858 private 14859 class FilePicker : Dialog { 14860 void delegate(string) onOK; 14861 void delegate() onCancel; 14862 LineEdit lineEdit; 14863 14864 // returns common prefix 14865 string loadFiles(string cwd, string[] filters...) { 14866 string[] files; 14867 string[] dirs; 14868 14869 string commonPrefix; 14870 14871 getFiles(cwd, (string name, bool isDirectory) { 14872 if(name == ".") 14873 return; // skip this as unnecessary 14874 if(isDirectory) 14875 dirs ~= name; 14876 else { 14877 foreach(filter; filters) 14878 if( 14879 filter.length <= 1 || 14880 filter == "*.*" || 14881 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 14882 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 14883 ) 14884 { 14885 files ~= name; 14886 14887 if(filter.length > 0 && filter[$-1] == '*') { 14888 if(commonPrefix is null) { 14889 commonPrefix = name; 14890 } else { 14891 foreach(idx, char i; name) { 14892 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 14893 commonPrefix = commonPrefix[0 .. idx]; 14894 break; 14895 } 14896 } 14897 } 14898 } 14899 14900 break; 14901 } 14902 } 14903 }); 14904 14905 extern(C) static int comparator(scope const void* a, scope const void* b) { 14906 auto sa = *cast(string*) a; 14907 auto sb = *cast(string*) b; 14908 14909 for(int i = 0; i < sa.length; i++) { 14910 if(i == sb.length) 14911 return 1; 14912 return sa[i] - sb[i]; 14913 } 14914 14915 return 0; 14916 } 14917 14918 nonPhobosSort(files, &comparator); 14919 nonPhobosSort(dirs, &comparator); 14920 14921 listWidget.clear(); 14922 dirWidget.clear(); 14923 foreach(name; dirs) 14924 dirWidget.addOption(name); 14925 foreach(name; files) 14926 listWidget.addOption(name); 14927 14928 return commonPrefix; 14929 } 14930 14931 ListWidget listWidget; 14932 ListWidget dirWidget; 14933 14934 string currentDirectory; 14935 string[] processedFilters; 14936 14937 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 14938 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 14939 super(300, 200, "Choose File..."); // owner); 14940 14941 foreach(filter; filters) { 14942 while(filter.length && filter[0] != 0) { 14943 filter = filter[1 .. $]; 14944 } 14945 if(filter.length) 14946 filter = filter[1 .. $]; // trim off the 0 14947 14948 while(filter.length) { 14949 int idx = 0; 14950 while(idx < filter.length && filter[idx] != ';') { 14951 idx++; 14952 } 14953 14954 processedFilters ~= filter[0 .. idx]; 14955 if(idx < filter.length) 14956 idx++; // skip the ; 14957 filter = filter[idx .. $]; 14958 } 14959 } 14960 14961 currentDirectory = initialDirectory is null ? "." : initialDirectory; 14962 14963 { 14964 auto hl = new HorizontalLayout(this); 14965 dirWidget = new ListWidget(hl); 14966 listWidget = new ListWidget(hl); 14967 14968 // double click events normally trigger something else but 14969 // here user might be clicking kinda fast and we'd rather just 14970 // keep it 14971 dirWidget.addEventListener((scope DoubleClickEvent dev) { 14972 auto ce = new ChangeEvent!void(dirWidget, () {}); 14973 ce.dispatch(); 14974 }); 14975 14976 dirWidget.addEventListener((scope ChangeEvent!void sce) { 14977 string v; 14978 foreach(o; dirWidget.options) 14979 if(o.selected) { 14980 v = o.label; 14981 break; 14982 } 14983 if(v.length) { 14984 currentDirectory ~= "/" ~ v; 14985 loadFiles(currentDirectory, processedFilters); 14986 } 14987 }); 14988 14989 // double click here, on the other hand, selects the file 14990 // and moves on 14991 listWidget.addEventListener((scope DoubleClickEvent dev) { 14992 OK(); 14993 }); 14994 } 14995 14996 lineEdit = new LineEdit(this); 14997 lineEdit.focus(); 14998 lineEdit.addEventListener(delegate(CharEvent event) { 14999 if(event.character == '\t' || event.character == '\n') 15000 event.preventDefault(); 15001 }); 15002 15003 listWidget.addEventListener(EventType.change, () { 15004 foreach(o; listWidget.options) 15005 if(o.selected) 15006 lineEdit.content = o.label; 15007 }); 15008 15009 loadFiles(currentDirectory, processedFilters); 15010 15011 lineEdit.addEventListener((KeyDownEvent event) { 15012 if(event.key == Key.Tab) { 15013 15014 auto current = lineEdit.content; 15015 if(current.length >= 2 && current[0 ..2] == "./") 15016 current = current[2 .. $]; 15017 15018 auto commonPrefix = loadFiles(".", current ~ "*"); 15019 15020 if(commonPrefix.length) 15021 lineEdit.content = commonPrefix; 15022 15023 // FIXME: if that is a directory, add the slash? or even go inside? 15024 15025 event.preventDefault(); 15026 } 15027 }); 15028 15029 lineEdit.content = prefilledName; 15030 15031 auto hl = new HorizontalLayout(60, this); 15032 auto cancelButton = new Button("Cancel", hl); 15033 auto okButton = new Button("OK", hl); 15034 15035 cancelButton.addEventListener(EventType.triggered, &Cancel); 15036 okButton.addEventListener(EventType.triggered, &OK); 15037 15038 this.addEventListener((KeyDownEvent event) { 15039 if(event.key == Key.Enter || event.key == Key.PadEnter) { 15040 event.preventDefault(); 15041 OK(); 15042 } 15043 if(event.key == Key.Escape) 15044 Cancel(); 15045 }); 15046 15047 } 15048 15049 override void OK() { 15050 if(lineEdit.content.length) { 15051 string accepted; 15052 auto c = lineEdit.content; 15053 if(c.length && c[0] == '/') 15054 accepted = c; 15055 else 15056 accepted = currentDirectory ~ "/" ~ lineEdit.content; 15057 15058 if(isDir(accepted)) { 15059 // FIXME: would be kinda nice to support ~ and collapse these paths too 15060 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 15061 currentDirectory = accepted; 15062 loadFiles(currentDirectory, processedFilters); 15063 lineEdit.content = ""; 15064 return; 15065 } 15066 15067 if(onOK) 15068 onOK(accepted); 15069 } 15070 close(); 15071 } 15072 15073 override void Cancel() { 15074 if(onCancel) 15075 onCancel(); 15076 close(); 15077 } 15078 } 15079 15080 private bool isDir(string name) { 15081 version(Windows) { 15082 auto ws = WCharzBuffer(name); 15083 auto ret = GetFileAttributesW(ws.ptr); 15084 if(ret == INVALID_FILE_ATTRIBUTES) 15085 return false; 15086 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 15087 } else version(Posix) { 15088 import core.sys.posix.sys.stat; 15089 stat_t buf; 15090 auto ret = stat((name ~ '\0').ptr, &buf); 15091 if(ret == -1) 15092 return false; // I could probably check more specific errors tbh 15093 return (buf.st_mode & S_IFMT) == S_IFDIR; 15094 } else return false; 15095 } 15096 15097 /* 15098 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 15099 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 15100 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 15101 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 15102 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 15103 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 15104 http://www.sbin.org/doc/Xlib/chapt_03.html 15105 15106 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 15107 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 15108 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 15109 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 15110 */ 15111 15112 15113 // These are all for setMenuAndToolbarFromAnnotatedCode 15114 /// This item in the menu will be preceded by a separator line 15115 /// Group: generating_from_code 15116 struct separator {} 15117 deprecated("It was misspelled, use separator instead") alias seperator = separator; 15118 /// Program-wide keyboard shortcut to trigger the action 15119 /// Group: generating_from_code 15120 struct accelerator { string keyString; } 15121 /// tells which menu the action will be on 15122 /// Group: generating_from_code 15123 struct menu { string name; } 15124 /// Describes which toolbar section the action appears on 15125 /// Group: generating_from_code 15126 struct toolbar { string groupName; } 15127 /// 15128 /// Group: generating_from_code 15129 struct icon { ushort id; } 15130 /// 15131 /// Group: generating_from_code 15132 struct label { string label; } 15133 /// 15134 /// Group: generating_from_code 15135 struct hotkey { dchar ch; } 15136 /// 15137 /// Group: generating_from_code 15138 struct tip { string tip; } 15139 15140 15141 /++ 15142 Observes and allows inspection of an object via automatic gui 15143 +/ 15144 /// Group: generating_from_code 15145 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 15146 return new ObjectInspectionWindowImpl!(T)(t); 15147 } 15148 15149 class ObjectInspectionWindow : Window { 15150 this(int a, int b, string c) { 15151 super(a, b, c); 15152 } 15153 15154 abstract void readUpdatesFromObject(); 15155 } 15156 15157 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 15158 T t; 15159 this(T t) { 15160 this.t = t; 15161 15162 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 15163 15164 foreach(memberName; __traits(derivedMembers, T)) {{ 15165 alias member = I!(__traits(getMember, t, memberName))[0]; 15166 alias type = typeof(member); 15167 static if(is(type == int)) { 15168 auto le = new LabeledLineEdit(memberName ~ ": ", this); 15169 //le.addEventListener("char", (Event ev) { 15170 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 15171 //ev.preventDefault(); 15172 //}); 15173 le.addEventListener(EventType.change, (Event ev) { 15174 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 15175 }); 15176 15177 updateMemberDelegates[memberName] = () { 15178 le.content = toInternal!string(__traits(getMember, t, memberName)); 15179 }; 15180 } 15181 }} 15182 } 15183 15184 void delegate()[string] updateMemberDelegates; 15185 15186 override void readUpdatesFromObject() { 15187 foreach(k, v; updateMemberDelegates) 15188 v(); 15189 } 15190 } 15191 15192 /++ 15193 Creates a dialog based on a data structure. 15194 15195 --- 15196 dialog((YourStructure value) { 15197 // the user filled in the struct and clicked OK, 15198 // you can check the members now 15199 }); 15200 --- 15201 15202 Params: 15203 initialData = the initial value to show in the dialog. It will not modify this unless 15204 it is a class then it might, no promises. 15205 15206 History: 15207 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 15208 +/ 15209 /// Group: generating_from_code 15210 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15211 dialog(T.init, onOK, onCancel, title); 15212 } 15213 /// ditto 15214 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15215 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 15216 dg.show(); 15217 } 15218 15219 private static template I(T...) { alias I = T; } 15220 15221 15222 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 15223 if(name == "id") 15224 return allLowerCase ? name : "ID"; 15225 15226 char[160] buffer; 15227 int bufferIndex = 0; 15228 bool shouldCap = true; 15229 bool shouldSpace; 15230 bool lastWasCap; 15231 foreach(idx, char ch; name) { 15232 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15233 15234 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 15235 if(lastWasCap) { 15236 // two caps in a row, don't change. Prolly acronym. 15237 } else { 15238 if(idx) 15239 shouldSpace = true; // new word, add space 15240 } 15241 15242 lastWasCap = true; 15243 } else { 15244 lastWasCap = false; 15245 } 15246 15247 if(shouldSpace) { 15248 buffer[bufferIndex++] = space; 15249 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15250 shouldSpace = false; 15251 } 15252 if(shouldCap) { 15253 if(ch >= 'a' && ch <= 'z') 15254 ch -= 32; 15255 shouldCap = false; 15256 } 15257 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15258 ch += 32; 15259 buffer[bufferIndex++] = ch; 15260 } 15261 return buffer[0 .. bufferIndex].idup; 15262 } 15263 15264 /++ 15265 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. 15266 +/ 15267 class AutomaticDialog(T) : Dialog { 15268 T t; 15269 15270 void delegate(T) onOK; 15271 void delegate() onCancel; 15272 15273 override int paddingTop() { return defaultLineHeight; } 15274 override int paddingBottom() { return defaultLineHeight; } 15275 override int paddingRight() { return defaultLineHeight; } 15276 override int paddingLeft() { return defaultLineHeight; } 15277 15278 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15279 assert(onOK !is null); 15280 15281 t = initialData; 15282 15283 static if(is(T == class)) { 15284 if(t is null) 15285 t = new T(); 15286 } 15287 this.onOK = onOK; 15288 this.onCancel = onCancel; 15289 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 15290 15291 static if(is(T == class)) 15292 this.addDataControllerWidget(t); 15293 else 15294 this.addDataControllerWidget(&t); 15295 15296 auto hl = new HorizontalLayout(this); 15297 auto stretch = new HorizontalSpacer(hl); // to right align 15298 auto ok = new CommandButton("OK", hl); 15299 auto cancel = new CommandButton("Cancel", hl); 15300 ok.addEventListener(EventType.triggered, &OK); 15301 cancel.addEventListener(EventType.triggered, &Cancel); 15302 15303 this.addEventListener((KeyDownEvent ev) { 15304 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15305 ok.focus(); 15306 OK(); 15307 ev.preventDefault(); 15308 } 15309 if(ev.key == Key.Escape) { 15310 Cancel(); 15311 ev.preventDefault(); 15312 } 15313 }); 15314 15315 this.addEventListener((scope ClosedEvent ce) { 15316 if(onCancel) 15317 onCancel(); 15318 }); 15319 15320 //this.children[0].focus(); 15321 } 15322 15323 override void OK() { 15324 onOK(t); 15325 close(); 15326 } 15327 15328 override void Cancel() { 15329 if(onCancel) 15330 onCancel(); 15331 close(); 15332 } 15333 } 15334 15335 private template baseClassCount(Class) { 15336 private int helper() { 15337 int count = 0; 15338 static if(is(Class bases == super)) { 15339 foreach(base; bases) 15340 static if(is(base == class)) 15341 count += 1 + baseClassCount!base; 15342 } 15343 return count; 15344 } 15345 15346 enum int baseClassCount = helper(); 15347 } 15348 15349 private long stringToLong(string s) { 15350 long ret; 15351 if(s.length == 0) 15352 return ret; 15353 bool negative = s[0] == '-'; 15354 if(negative) 15355 s = s[1 .. $]; 15356 foreach(ch; s) { 15357 if(ch >= '0' && ch <= '9') { 15358 ret *= 10; 15359 ret += ch - '0'; 15360 } 15361 } 15362 if(negative) 15363 ret = -ret; 15364 return ret; 15365 } 15366 15367 15368 interface ReflectableProperties { 15369 /++ 15370 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15371 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15372 json in the current implementation. 15373 15374 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15375 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15376 as of the June 2, 2021 release. 15377 15378 History: 15379 Added June 2, 2021. 15380 15381 See_Also: [getPropertyAsString], [setPropertyFromString] 15382 +/ 15383 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15384 /++ 15385 Requests a property to be delivered to you as a string, through your `sink` delegate. 15386 15387 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15388 be interpreted as json, otherwise, it is just a plain string. 15389 15390 The sink should always be called exactly once for each call (it is basically a return value, but it might 15391 use a local buffer it maintains instead of allocating a return value). 15392 15393 History: 15394 Added June 2, 2021. 15395 15396 See_Also: [getPropertiesList], [setPropertyFromString] 15397 +/ 15398 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15399 /++ 15400 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. 15401 15402 History: 15403 Added June 2, 2021. 15404 15405 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15406 +/ 15407 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15408 15409 /// [setPropertyFromString] possible return values 15410 enum SetPropertyResult { 15411 success = 0, /// the property has been successfully set to the request value 15412 notPermitted = -1, /// the property exists but it cannot be changed at this time 15413 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15414 noSuchProperty = -3, /// there is no property by that name 15415 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15416 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) 15417 } 15418 15419 /++ 15420 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15421 15422 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15423 15424 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15425 rarely need to use these building blocks directly. 15426 +/ 15427 mixin template RegisterSetters() { 15428 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15429 switch(name) { 15430 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15431 case memberName: 15432 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15433 if(value != "true" && value != "false") 15434 return SetPropertyResult.wrongFormat; 15435 __traits(getMember, this, memberName) = value == "true" ? true : false; 15436 return SetPropertyResult.success; 15437 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15438 import core.stdc.stdlib; 15439 char[128] zero = 0; 15440 if(buffer.length + 1 >= zero.length) 15441 return SetPropertyResult.wrongFormat; 15442 zero[0 .. buffer.length] = buffer[]; 15443 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15444 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15445 import core.stdc.stdlib; 15446 char[128] zero = 0; 15447 if(buffer.length + 1 >= zero.length) 15448 return SetPropertyResult.wrongFormat; 15449 zero[0 .. buffer.length] = buffer[]; 15450 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15451 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15452 __traits(getMember, this, memberName) = value.idup; 15453 } else { 15454 return SetPropertyResult.notImplemented; 15455 } 15456 15457 } 15458 default: 15459 return super.setPropertyFromString(name, value, valueIsJson); 15460 } 15461 } 15462 } 15463 15464 /++ 15465 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15466 15467 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15468 15469 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15470 rarely need to use these building blocks directly. 15471 +/ 15472 mixin template RegisterGetters() { 15473 override void getPropertiesList(scope void delegate(string name) sink) const { 15474 super.getPropertiesList(sink); 15475 15476 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15477 sink(memberName); 15478 } 15479 } 15480 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15481 switch(name) { 15482 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15483 case memberName: 15484 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15485 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15486 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15487 import core.stdc.stdio; 15488 char[32] buffer; 15489 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15490 sink(name, buffer[0 .. len], true); 15491 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15492 import core.stdc.stdio; 15493 char[32] buffer; 15494 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15495 sink(name, buffer[0 .. len], true); 15496 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15497 sink(name, __traits(getMember, this, memberName), false); 15498 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15499 } else { 15500 sink(name, null, true); 15501 } 15502 15503 return; 15504 } 15505 default: 15506 return super.getPropertyAsString(name, sink); 15507 } 15508 } 15509 } 15510 } 15511 15512 private struct Stack(T) { 15513 this(int maxSize) { 15514 internalLength = 0; 15515 arr = initialBuffer[]; 15516 } 15517 15518 ///. 15519 void push(T t) { 15520 if(internalLength >= arr.length) { 15521 auto oldarr = arr; 15522 if(arr.length < 4096) 15523 arr = new T[arr.length * 2]; 15524 else 15525 arr = new T[arr.length + 4096]; 15526 arr[0 .. oldarr.length] = oldarr[]; 15527 } 15528 15529 arr[internalLength] = t; 15530 internalLength++; 15531 } 15532 15533 ///. 15534 T pop() { 15535 assert(internalLength); 15536 internalLength--; 15537 return arr[internalLength]; 15538 } 15539 15540 ///. 15541 T peek() { 15542 assert(internalLength); 15543 return arr[internalLength - 1]; 15544 } 15545 15546 ///. 15547 @property bool empty() { 15548 return internalLength ? false : true; 15549 } 15550 15551 ///. 15552 private T[] arr; 15553 private size_t internalLength; 15554 private T[64] initialBuffer; 15555 // 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), 15556 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 15557 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 15558 } 15559 15560 /// 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. 15561 private struct WidgetStream { 15562 15563 ///. 15564 @property Widget front() { 15565 return current.widget; 15566 } 15567 15568 /// Use Widget.tree instead. 15569 this(Widget start) { 15570 current.widget = start; 15571 current.childPosition = -1; 15572 isEmpty = false; 15573 stack = typeof(stack)(0); 15574 } 15575 15576 /* 15577 Handle it 15578 handle its children 15579 15580 */ 15581 15582 ///. 15583 void popFront() { 15584 more: 15585 if(isEmpty) return; 15586 15587 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 15588 15589 current.childPosition++; 15590 if(current.childPosition >= current.widget.children.length) { 15591 if(stack.empty()) 15592 isEmpty = true; 15593 else { 15594 current = stack.pop(); 15595 goto more; 15596 } 15597 } else { 15598 stack.push(current); 15599 current.widget = current.widget.children[current.childPosition]; 15600 current.childPosition = -1; 15601 } 15602 } 15603 15604 ///. 15605 @property bool empty() { 15606 return isEmpty; 15607 } 15608 15609 private: 15610 15611 struct Current { 15612 Widget widget; 15613 int childPosition; 15614 } 15615 15616 Current current; 15617 15618 Stack!(Current) stack; 15619 15620 bool isEmpty; 15621 } 15622 15623 15624 /+ 15625 15626 I could fix up the hierarchy kinda like this 15627 15628 class Widget { 15629 Widget[] children() { return null; } 15630 } 15631 interface WidgetContainer { 15632 Widget asWidget(); 15633 void addChild(Widget w); 15634 15635 // alias asWidget this; // but meh 15636 } 15637 15638 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 15639 15640 class Layout : Widget, WidgetContainer {} 15641 15642 class Window : WidgetContainer {} 15643 15644 15645 All constructors that previously took Widgets should now take WidgetContainers instead 15646 15647 15648 15649 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". 15650 +/ 15651 15652 /+ 15653 LAYOUTS 2.0 15654 15655 can just be assigned as a function. assigning a new one will cause it to be immediately called. 15656 15657 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 15658 15659 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 15660 15661 and even Paint can just use computedStyle... 15662 15663 background color 15664 font 15665 border color and style 15666 15667 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 15668 please note that many widgets and in some modes will completely ignore properties as they will. 15669 they are just hints you set, not promises. 15670 15671 15672 15673 15674 15675 So generally the existing virtual functions are just the default for the class. But individual objects 15676 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 15677 +/ 15678 15679 /++ 15680 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. 15681 15682 History: 15683 Added May 24, 2021. 15684 +/ 15685 struct WidgetBackground { 15686 /++ 15687 A background with the given solid color. 15688 +/ 15689 this(Color color) { 15690 this.color = color; 15691 } 15692 15693 this(WidgetBackground bg) { 15694 this = bg; 15695 } 15696 15697 /++ 15698 Creates a widget from the string. 15699 15700 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 15701 +/ 15702 static WidgetBackground fromString(string s) { 15703 return WidgetBackground(Color.fromString(s)); 15704 } 15705 15706 /++ 15707 The background is not necessarily a solid color, but you can always specify a color as a fallback. 15708 15709 History: 15710 Made `public` on December 18, 2022 (dub v10.10). 15711 +/ 15712 Color color; 15713 } 15714 15715 /++ 15716 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!) 15717 15718 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. 15719 15720 You should not inherit from this directly, but instead use [VisualTheme]. 15721 15722 History: 15723 Added May 8, 2021 15724 +/ 15725 abstract class BaseVisualTheme { 15726 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 15727 abstract void doPaint(Widget widget, WidgetPainter painter); 15728 15729 /+ 15730 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 15731 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 15732 +/ 15733 15734 /++ 15735 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 15736 where the interpretation of the string varies for each property and may include things like measurement units. 15737 +/ 15738 abstract string getPropertyString(Widget widget, string propertyName); 15739 15740 /++ 15741 Default background color of the window. Widgets also use this to simulate transparency. 15742 15743 Probably some shade of grey. 15744 +/ 15745 abstract Color windowBackgroundColor(); 15746 abstract Color widgetBackgroundColor(); 15747 abstract Color foregroundColor(); 15748 abstract Color lightAccentColor(); 15749 abstract Color darkAccentColor(); 15750 15751 /++ 15752 Colors used to indicate active selections in lists and text boxes, etc. 15753 +/ 15754 abstract Color selectionForegroundColor(); 15755 /// ditto 15756 abstract Color selectionBackgroundColor(); 15757 15758 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 15759 15760 /++ 15761 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 15762 +/ 15763 abstract OperatingSystemFont defaultFont(int dpi); 15764 15765 private OperatingSystemFont[int] defaultFontCache_; 15766 private OperatingSystemFont defaultFontCached(int dpi) { 15767 if(dpi !in defaultFontCache_) { 15768 // FIXME: set this to false if X disconnect or if visual theme changes 15769 defaultFontCache_[dpi] = defaultFont(dpi); 15770 } 15771 return defaultFontCache_[dpi]; 15772 } 15773 } 15774 15775 /+ 15776 A widget should have: 15777 classList 15778 dataset 15779 attributes 15780 computedStyles 15781 state (persistent) 15782 dynamic state (focused, hover, etc) 15783 +/ 15784 15785 // visualTheme.computedStyle(this).paddingLeft 15786 15787 15788 /++ 15789 This is your entry point to create your own visual theme for custom widgets. 15790 15791 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 15792 15793 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 15794 +/ 15795 abstract class VisualTheme(CRTP) : BaseVisualTheme { 15796 override string getPropertyString(Widget widget, string propertyName) { 15797 return null; 15798 } 15799 15800 /+ 15801 mixin StyleOverride!Widget 15802 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 15803 w.useStyleProperties(dg); 15804 } 15805 +/ 15806 15807 final override void doPaint(Widget widget, WidgetPainter painter) { 15808 auto derived = cast(CRTP) cast(void*) this; 15809 15810 scope void delegate(Widget, WidgetPainter) bestMatch; 15811 int bestMatchScore; 15812 15813 static if(__traits(hasMember, CRTP, "paint")) 15814 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 15815 static if(is(typeof(overload) Params == __parameters)) { 15816 static assert(Params.length == 2); 15817 static assert(is(Params[0] : Widget)); 15818 static assert(is(Params[1] == WidgetPainter)); 15819 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); 15820 15821 alias type = Params[0]; 15822 if(cast(type) widget) { 15823 auto score = baseClassCount!type; 15824 15825 if(score > bestMatchScore) { 15826 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 15827 bestMatchScore = score; 15828 } 15829 } 15830 } else static assert(0, "paint should be a method."); 15831 } 15832 15833 if(bestMatch) 15834 bestMatch(widget, painter); 15835 else 15836 widget.paint(painter); 15837 } 15838 15839 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 15840 15841 // 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 15842 // mixin Beautiful95Theme; 15843 mixin DefaultLightTheme; 15844 15845 private static struct Cached { 15846 // i prolly want to do this 15847 } 15848 } 15849 15850 /// ditto 15851 mixin template Beautiful95Theme() { 15852 override Color windowBackgroundColor() { return Color(212, 212, 212); } 15853 override Color widgetBackgroundColor() { return Color.white; } 15854 override Color foregroundColor() { return Color.black; } 15855 override Color darkAccentColor() { return Color(172, 172, 172); } 15856 override Color lightAccentColor() { return Color(223, 223, 223); } 15857 override Color selectionForegroundColor() { return Color.white; } 15858 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15859 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 15860 } 15861 15862 /// ditto 15863 mixin template DefaultLightTheme() { 15864 override Color windowBackgroundColor() { return Color(232, 232, 232); } 15865 override Color widgetBackgroundColor() { return Color.white; } 15866 override Color foregroundColor() { return Color.black; } 15867 override Color darkAccentColor() { return Color(172, 172, 172); } 15868 override Color lightAccentColor() { return Color(223, 223, 223); } 15869 override Color selectionForegroundColor() { return Color.white; } 15870 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15871 override OperatingSystemFont defaultFont(int dpi) { 15872 version(Windows) 15873 return new OperatingSystemFont("Segoe UI"); 15874 else { 15875 // FIXME: undo xft's scaling so we don't end up double scaled 15876 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15877 } 15878 } 15879 } 15880 15881 /// ditto 15882 mixin template DefaultDarkTheme() { 15883 override Color windowBackgroundColor() { return Color(64, 64, 64); } 15884 override Color widgetBackgroundColor() { return Color.black; } 15885 override Color foregroundColor() { return Color.white; } 15886 override Color darkAccentColor() { return Color(20, 20, 20); } 15887 override Color lightAccentColor() { return Color(80, 80, 80); } 15888 override Color selectionForegroundColor() { return Color.white; } 15889 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 15890 override OperatingSystemFont defaultFont(int dpi) { 15891 version(Windows) 15892 return new OperatingSystemFont("Segoe UI", 12); 15893 else 15894 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15895 } 15896 } 15897 15898 /// ditto 15899 alias DefaultTheme = DefaultLightTheme; 15900 15901 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 15902 /+ 15903 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 15904 Color windowBackgroundColor() { return Color(242, 242, 242); } 15905 Color darkAccentColor() { return windowBackgroundColor; } 15906 Color lightAccentColor() { return windowBackgroundColor; } 15907 +/ 15908 } 15909 15910 /++ 15911 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 15912 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 15913 15914 History: 15915 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15916 +/ 15917 class StateChanged(alias field) : Event { 15918 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 15919 override bool cancelable() const { return false; } 15920 this(Widget target, typeof(field) newValue) { 15921 this.newValue = newValue; 15922 super(EventString, target); 15923 } 15924 15925 typeof(field) newValue; 15926 } 15927 15928 /++ 15929 Convenience function to add a `triggered` event listener. 15930 15931 Its implementation is simply `w.addEventListener("triggered", dg);` 15932 15933 History: 15934 Added November 27, 2021 (dub v10.4) 15935 +/ 15936 void addWhenTriggered(Widget w, void delegate() dg) { 15937 w.addEventListener("triggered", dg); 15938 } 15939 15940 /++ 15941 Observable varables can be added to widgets and when they are changed, it fires 15942 off a [StateChanged] event so you can react to it. 15943 15944 It is implemented as a getter and setter property, along with another helper you 15945 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 15946 event through the usual means. Just give the name of the variable. See [StateChanged] for an 15947 example. 15948 15949 History: 15950 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15951 +/ 15952 mixin template Observable(T, string name) { 15953 private T backing; 15954 15955 mixin(q{ 15956 void } ~ name ~ q{_changed (void delegate(T) dg) { 15957 this.addEventListener((StateChanged!this_thing ev) { 15958 dg(ev.newValue); 15959 }); 15960 } 15961 15962 @property T } ~ name ~ q{ () { 15963 return backing; 15964 } 15965 15966 @property void } ~ name ~ q{ (T t) { 15967 backing = t; 15968 auto event = new StateChanged!this_thing(this, t); 15969 event.dispatch(); 15970 } 15971 }); 15972 15973 mixin("private alias this_thing = " ~ name ~ ";"); 15974 } 15975 15976 15977 private bool startsWith(string test, string thing) { 15978 if(test.length < thing.length) 15979 return false; 15980 return test[0 .. thing.length] == thing; 15981 } 15982 15983 private bool endsWith(string test, string thing) { 15984 if(test.length < thing.length) 15985 return false; 15986 return test[$ - thing.length .. $] == thing; 15987 } 15988 15989 // still do layout delegation 15990 // and... split off Window from Widget. 15991 15992 version(minigui_screenshots) 15993 struct Screenshot { 15994 string name; 15995 } 15996 15997 version(minigui_screenshots) 15998 static if(__VERSION__ > 2092) 15999 mixin(q{ 16000 shared static this() { 16001 import core.runtime; 16002 16003 static UnitTestResult screenshotMagic() { 16004 string name; 16005 16006 import arsd.png; 16007 16008 auto results = new Window(); 16009 auto button = new Button("do it", results); 16010 16011 Window.newWindowCreated = delegate(Window w) { 16012 Timer timer; 16013 timer = new Timer(250, { 16014 auto img = w.win.takeScreenshot(); 16015 timer.destroy(); 16016 16017 version(Windows) 16018 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 16019 else 16020 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 16021 16022 w.close(); 16023 }); 16024 }; 16025 16026 button.addWhenTriggered( { 16027 16028 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 16029 name = null; 16030 static foreach(attr; __traits(getAttributes, test)) { 16031 static if(is(typeof(attr) == Screenshot)) 16032 name = attr.name; 16033 } 16034 if(name.length) { 16035 test(); 16036 } 16037 } 16038 16039 }); 16040 16041 results.loop(); 16042 16043 return UnitTestResult(0, 0, false, false); 16044 } 16045 16046 16047 Runtime.extendedModuleUnitTester = &screenshotMagic; 16048 } 16049 }); 16050 version(minigui_screenshots) { 16051 version(unittest) 16052 void main() {} 16053 else static assert(0, "dont forget the -unittest flag to dmd"); 16054 } 16055 16056 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 16057 // FIXME: make multiple accelerators disambiguate based ona rgs 16058 // FIXME: MainWindow ctor should have same arg order as Window 16059 // FIXME: mainwindow ctor w/ client area size instead of total size. 16060 // 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. 16061 // FIXME: tri-state checkbox 16062 // FIXME: subordinate controls grouping...