1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 /* 4 5 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 6 7 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 8 */ 9 10 // FIXME: opt-in file picker widget with image support 11 12 // FIXME: slider widget. 13 // FIXME: number widget 14 15 // osx style menu search. 16 17 // would be cool for a scroll bar to have marking capabilities 18 // kinda like vim's marks just on clicks etc and visual representation 19 // generically. may be cool to add an up arrow to the bottom too 20 // 21 // leave a shadow of where you last were for going back easily 22 23 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 24 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 25 // the window. 26 27 // so what about context menus? 28 29 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 30 31 // FIXME: make the scroll thing go to bottom when the content changes. 32 33 // 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 34 35 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 36 37 38 // FIXME: add a command search thingy built in and implement tip. 39 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 40 41 // On Windows: 42 // FIXME: various labels look broken in high contrast mode 43 // FIXME: changing themes while the program is upen doesn't trigger a redraw 44 45 // add note about manifest to documentation. also icons. 46 47 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 48 // FIXME: clear the corner of scrollbars if they pop up 49 50 // minigui needs to have a stdout redirection for gui mode on windows writeln 51 52 // I kinda wanna do state reacting. sort of. idk tho 53 54 // need a viewer widget that works like a web page - arrows scroll down consistently 55 56 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 57 58 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 59 // and help info about menu items. 60 // and search in menus? 61 62 // FIXME: a scroll area event signaling when a thing comes into view might be good 63 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 64 65 // FIXME: unify Windows style line endings 66 67 /* 68 TODO: 69 70 pie menu 71 72 class Form with submit behavior -- see AutomaticDialog 73 74 disabled widgets and menu items 75 76 event cleanup 77 tooltips. 78 api improvements 79 80 margins are kinda broken, they don't collapse like they should. at least. 81 82 a table form btw would be a horizontal layout of vertical layouts holding each column 83 that would give the same width things 84 */ 85 86 /* 87 88 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 89 */ 90 91 /++ 92 minigui is a smallish GUI widget library, aiming to be on par with at least 93 HTML4 forms and a few other expected gui components. It uses native controls 94 on Windows and does its own thing on Linux (Mac is not currently supported but 95 may be later, and should use native controls) to keep size down. The Linux 96 appearance is similar to Windows 95 and avoids using images to maintain network 97 efficiency on remote X connections, though you can customize that. 98 99 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color]. 100 101 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 102 It isn't hugely concerned with appearance - on Windows, it just uses the native 103 controls and native theme, and on Linux, it keeps it simple and I may change that 104 at any time. 105 106 I love Qt, if you want something full featured, use it! But if you want something 107 you can just drop into a small project and expect the basics to work without outside 108 dependencies, hopefully minigui will work for you. 109 110 The event model is similar to what you use in the browser with Javascript and the 111 layout engine tries to automatically fit things in, similar to a css flexbox. 112 113 114 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 115 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 116 console and other visual bugs. 117 118 HTML_To_Classes: 119 $(SMALL_TABLE 120 HTML Code | Minigui Class 121 122 `<input type="text">` | [LineEdit] 123 `<textarea>` | [TextEdit] 124 `<select>` | [DropDownSelection] 125 `<input type="checkbox">` | [Checkbox] 126 `<input type="radio">` | [Radiobox] 127 `<button>` | [Button] 128 ) 129 130 131 Stretchiness: 132 The default is 4. You can use larger numbers for things that should 133 consume a lot of space, and lower numbers for ones that are better at 134 smaller sizes. 135 136 Overlapped_input: 137 COMING EVENTUALLY: 138 minigui will include a little bit of I/O functionality that just works 139 with the event loop. If you want to get fancy, I suggest spinning up 140 another thread and posting events back and forth. 141 142 $(H2 Add ons) 143 144 $(H2 XML definitions) 145 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 146 147 $(H3 Scriptability) 148 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 149 in this documentation, it means you can call it from the script language. 150 151 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 152 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 153 154 --- 155 import arsd.minigui_xml; 156 import arsd.script; 157 158 var globals = var.emptyObject; 159 globals.makeWidgetFromString = &makeWidgetFromString; 160 161 // this now works 162 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 163 --- 164 165 More to come. 166 167 History: 168 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 169 170 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 171 tag this as version 2.0. 172 173 Among the changes: 174 $(LIST 175 * 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. 176 177 See [Event] for details. 178 179 * 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. 180 181 See [DoubleClickEvent] for details. 182 183 * 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. 184 185 See [Widget.Style] for details. 186 187 * 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. 188 189 * Various non-breaking additions. 190 ) 191 +/ 192 module arsd.minigui; 193 194 public import arsd.simpledisplay; 195 private alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 196 197 version(Windows) { 198 import core.sys.windows.winnls; 199 import core.sys.windows.windef; 200 import core.sys.windows.basetyps; 201 import core.sys.windows.winbase; 202 import core.sys.windows.winuser; 203 import core.sys.windows.wingdi; 204 static import gdi = core.sys.windows.wingdi; 205 } 206 207 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 208 private bool lastDefaultPrevented; 209 210 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 211 alias scriptable = arsd_jsvar_compatible; 212 213 version(Windows) { 214 // use native widgets when available unless specifically asked otherwise 215 version(custom_widgets) { 216 enum bool UsingCustomWidgets = true; 217 enum bool UsingWin32Widgets = false; 218 } else { 219 version = win32_widgets; 220 enum bool UsingCustomWidgets = false; 221 enum bool UsingWin32Widgets = true; 222 } 223 // and native theming when needed 224 //version = win32_theming; 225 } else { 226 enum bool UsingCustomWidgets = true; 227 enum bool UsingWin32Widgets = false; 228 version=custom_widgets; 229 } 230 231 232 233 /* 234 235 The main goals of minigui.d are to: 236 1) Provide basic widgets that just work in a lightweight lib. 237 I basically want things comparable to a plain HTML form, 238 plus the easy and obvious things you expect from Windows 239 apps like a menu. 240 2) Use native things when possible for best functionality with 241 least library weight. 242 3) Give building blocks to provide easy extension for your 243 custom widgets, or hooking into additional native widgets 244 I didn't wrap. 245 4) Provide interfaces for easy interaction between third 246 party minigui extensions. (event model, perhaps 247 signals/slots, drop-in ease of use bits.) 248 5) Zero non-system dependencies, including Phobos as much as 249 I reasonably can. It must only import arsd.color and 250 my simpledisplay.d. If you need more, it will have to be 251 an extension module. 252 6) An easy layout system that generally works. 253 254 A stretch goal is to make it easy to make gui forms with code, 255 some kind of resource file (xml?) and even a wysiwyg designer. 256 257 Another stretch goal is to make it easy to hook data into the gui, 258 including from reflection. So like auto-generate a form from a 259 function signature or struct definition, or show a list from an 260 array that automatically updates as the array is changed. Then, 261 your program focuses on the data more than the gui interaction. 262 263 264 265 STILL NEEDED: 266 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 267 * slider 268 * listbox 269 * spinner 270 * label? 271 * rich text 272 */ 273 274 275 /+ 276 enum LayoutMethods { 277 verticalFlex, 278 horizontalFlex, 279 inlineBlock, // left to right, no stretch, goes to next line as needed 280 static, // just set to x, y 281 verticalNoStretch, // browser style default 282 283 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 284 285 grid, // magic 286 } 287 +/ 288 289 /++ 290 The way this module works is it builds on top of a SimpleWindow 291 from simpledisplay to provide simple controls and such. 292 293 Non-native controls suck, but nevertheless, I'm going to do it that 294 way to avoid dependencies on stuff like gtk on X... and since I'll 295 be writing the widgets there, I might as well just use them on Windows 296 too if you like, using `-version=custom_widgets`. 297 298 So, by extension, this sucks. But gtkd is just too big for me. 299 300 301 The goal is to look kinda like Windows 95, perhaps with customizability. 302 Nothing too fancy, just the basics that work, but of course you can do whatever 303 you want in the draw handler. 304 +/ 305 class Widget { 306 307 // Default layout properties { 308 309 int minWidth() { return 0; } 310 int minHeight() { 311 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 312 int sum = 0; 313 foreach(child; children) { 314 sum += child.minHeight(); 315 sum += child.marginTop(); 316 sum += child.marginBottom(); 317 } 318 319 return sum; 320 } 321 int maxWidth() { return int.max; } 322 int maxHeight() { return int.max; } 323 int widthStretchiness() { return 4; } 324 int heightStretchiness() { return 4; } 325 326 int marginLeft() { return 0; } 327 int marginRight() { return 0; } 328 int marginTop() { return 0; } 329 int marginBottom() { return 0; } 330 int paddingLeft() { return 0; } 331 int paddingRight() { return 0; } 332 int paddingTop() { return 0; } 333 int paddingBottom() { return 0; } 334 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 335 336 void recomputeChildLayout() { 337 .recomputeChildLayout!"height"(this); 338 } 339 340 // } 341 342 343 /++ 344 Returns the style's tag name string this object uses. 345 346 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. 347 348 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 349 350 History: 351 Added May 10, 2021 352 +/ 353 string styleTagName() const { 354 string n = typeid(this).name; 355 foreach_reverse(idx, ch; n) 356 if(ch == '.') { 357 n = n[idx + 1 .. $]; 358 break; 359 } 360 return n; 361 } 362 363 /// API for the [styleClassList] 364 static struct ClassList { 365 private Widget widget; 366 367 /// 368 void add(string s) { 369 widget.styleClassList_ ~= s; 370 } 371 372 /// 373 void remove(string s) { 374 foreach(idx, s1; widget.styleClassList_) 375 if(s1 == s) { 376 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 377 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 378 widget.styleClassList_.assumeSafeAppend(); 379 return; 380 } 381 } 382 383 /// Returns true if it was added, false if it was removed. 384 bool toggle(string s) { 385 if(contains(s)) { 386 remove(s); 387 return false; 388 } else { 389 add(s); 390 return true; 391 } 392 } 393 394 /// 395 bool contains(string s) const { 396 foreach(s1; widget.styleClassList_) 397 if(s1 == s) 398 return true; 399 return false; 400 401 } 402 } 403 404 private string[] styleClassList_; 405 406 /++ 407 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. 408 409 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 410 411 History: 412 Added May 10, 2021 413 +/ 414 inout(ClassList) styleClassList() inout { 415 return cast(inout(ClassList)) ClassList(cast() this); 416 } 417 418 /++ 419 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. 420 421 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. 422 423 The upper 32 bits are available for your own extensions. 424 425 History: 426 Added May 10, 2021 427 +/ 428 enum DynamicState : ulong { 429 focus = (1 << 0), /// the widget currently has the keyboard focus 430 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 431 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 432 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 433 checked = (1 << 4), /// the widget is toggleable and currently toggled on 434 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 435 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 436 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 437 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. 438 439 USER_BEGIN = (1UL << 32), 440 } 441 442 // I want to add the primary and cancel styles to buttons at least at some point somehow. 443 444 /// ditto 445 @property ulong dynamicState() { return dynamicState_; } 446 /// ditto 447 @property ulong dynamicState(ulong newValue) { 448 if(dynamicState != newValue) { 449 auto old = dynamicState_; 450 dynamicState_ = newValue; 451 452 useStyleProperties((s) { 453 if(s.variesWithState(old ^ newValue)) 454 redraw(); 455 }); 456 } 457 return dynamicState_; 458 } 459 460 /// ditto 461 void setDynamicState(ulong flags, bool state) { 462 auto ds = dynamicState_; 463 if(state) 464 ds |= flags; 465 else 466 ds &= ~flags; 467 468 dynamicState = ds; 469 } 470 471 private ulong dynamicState_; 472 473 deprecated("Use dynamic styles instead now") { 474 Color backgroundColor() { return backgroundColor_; } 475 void backgroundColor(Color c){ this.backgroundColor_ = c; } 476 477 MouseCursor cursor() { return GenericCursor.Default; } 478 } private Color backgroundColor_ = Color.transparent; 479 480 481 /++ 482 Style properties are defined as an accessory class so they can be referenced and overridden independently. 483 484 It is here so there can be a specificity switch. 485 486 History: 487 Added May 11, 2021 488 +/ 489 static class Style/* : StyleProperties*/ { 490 public Widget widget; // public because the mixin template needs access to it 491 492 /// This assumes any change to the dynamic state (focus, hover, etc) triggers a redraw, but you can filter a bit to optimize some draws. 493 bool variesWithState(ulong dynamicStateFlags) { 494 return true; 495 } 496 497 /// 498 Color foregroundColor() { 499 return WidgetPainter.visualTheme.foregroundColor; 500 } 501 502 /// 503 Color backgroundColor() { 504 // the default is a "transparent" background, which means 505 // it goes as far up as it can to get the color 506 if (widget.backgroundColor_ != Color.transparent) 507 return widget.backgroundColor_; 508 if (widget.parent) 509 return StyleInformation.extractStyleProperty!"backgroundColor"(widget.parent); 510 return widget.backgroundColor_; 511 } 512 513 private OperatingSystemFont fontCached_; 514 private OperatingSystemFont fontCached() { 515 if(fontCached_ is null) 516 fontCached_ = font(); 517 return fontCached_; 518 } 519 520 /++ 521 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. 522 +/ 523 OperatingSystemFont font() { 524 return null; 525 } 526 527 /++ 528 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. 529 530 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 531 532 History: 533 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 534 +/ 535 MouseCursor cursor() { 536 return GenericCursor.Default; 537 } 538 539 FrameStyle borderStyle() { 540 return FrameStyle.none; 541 } 542 543 /++ 544 +/ 545 Color borderColor() { 546 return Color.transparent; 547 } 548 549 FrameStyle outlineStyle() { 550 if(widget.dynamicState & DynamicState.focus) 551 return FrameStyle.dotted; 552 else 553 return FrameStyle.none; 554 } 555 556 Color outlineColor() { 557 return foregroundColor; 558 } 559 } 560 561 /++ 562 This goes in order like this: 563 564 mixin OverrideStyle!( 565 DynamicState.focus, YourFocusedStyle, 566 DynamicState.hover, YourHoverStyle, 567 YourDefaultStyle 568 ) 569 570 It checks if `dynamicState` matches the state and if so, returns the object given. 571 572 If there is no state mask given, the next one matches everything. The first match 573 given is used. 574 +/ 575 static protected mixin template OverrideStyle(S...) { 576 override void useStyleProperties(scope void delegate(scope Widget.Style props) dg) { 577 ulong mask = 0; 578 foreach(idx, thing; S) { 579 static if(is(typeof(thing) : ulong)) { 580 mask = thing; 581 } else { 582 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 583 scope Widget.Style s = new thing(); 584 s.widget = this; 585 dg(s); 586 return; 587 } 588 } 589 } 590 } 591 } 592 /++ 593 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 594 +/ 595 void useStyleProperties(scope void delegate(scope Style props) dg) { 596 scope Style s = new Style(); 597 s.widget = this; 598 dg(s); 599 } 600 601 602 protected void sendResizeEvent() { 603 this.emit!ResizeEvent(); 604 } 605 606 Menu contextMenu(int x, int y) { return null; } 607 608 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 609 if(parentWindow is null || parentWindow.win is null) return false; 610 611 auto menu = this.contextMenu(x, y); 612 if(menu is null) 613 return false; 614 615 version(win32_widgets) { 616 // FIXME: if it is -1, -1, do it at the current selection location instead 617 // tho the corner of the window, whcih it does now, isn't the literal worst. 618 619 if(screenX < 0 && screenY < 0) { 620 auto p = this.globalCoordinates(); 621 if(screenX == -2) 622 p.x += x; 623 if(screenY == -2) 624 p.y += y; 625 626 screenX = p.x; 627 screenY = p.y; 628 } 629 630 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 631 throw new Exception("TrackContextMenuEx"); 632 } else version(custom_widgets) { 633 menu.popup(this, x, y); 634 } 635 636 return true; 637 } 638 639 /++ 640 Removes this widget from its parent. 641 642 History: 643 `removeWidget` was made `final` on May 11, 2021. 644 +/ 645 @scriptable 646 final void removeWidget() { 647 auto p = this.parent; 648 if(p) { 649 int item; 650 for(item = 0; item < p._children.length; item++) 651 if(p._children[item] is this) 652 break; 653 for(; item < p._children.length - 1; item++) 654 p._children[item] = p._children[item + 1]; 655 p._children = p._children[0 .. $-1]; 656 } 657 } 658 659 /++ 660 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 661 +/ 662 @scriptable 663 Widget getChildByName(string name) { 664 return getByName(name); 665 } 666 /++ 667 Finds the nearest descendant with the requested type and [name]. May return `this`. 668 +/ 669 final WidgetClass getByName(WidgetClass = Widget)(string name) { 670 if(this.name == name) 671 if(auto c = cast(WidgetClass) this) 672 return c; 673 foreach(child; children) { 674 auto w = child.getByName(name); 675 if(auto c = cast(WidgetClass) w) 676 return c; 677 } 678 return null; 679 } 680 681 /++ 682 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. 683 Names should be unique in a window. 684 685 See_Also: [getByName], [getChildByName] 686 +/ 687 @scriptable string name; 688 689 private EventHandler[][string] bubblingEventHandlers; 690 private EventHandler[][string] capturingEventHandlers; 691 692 /++ 693 Default event handlers. These are called on the appropriate 694 event unless [Event.preventDefault] is called on the event at 695 some point through the bubbling process. 696 697 698 If you are implementing your own widget and want to add custom 699 events, you should follow the same pattern here: create a virtual 700 function named `defaultEventHandler_eventname` with the implementation, 701 then, override [setupDefaultEventHandlers] and add a wrapped caller to 702 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 703 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 704 This ensures virtual dispatch based on the correct subclass. 705 706 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 707 overridden version. 708 709 You only need to do that on parent classes adding NEW event types. If you 710 just want to change the default behavior of an existing event type in a subclass, 711 you override the function (and optionally call `super.method_name`) like normal. 712 713 +/ 714 protected EventHandler[string] defaultEventHandlers; 715 716 /// ditto 717 void setupDefaultEventHandlers() { 718 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 719 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 720 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 721 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 722 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 723 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 724 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 725 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 726 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 727 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 728 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 729 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 730 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 731 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 732 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 733 } 734 735 /// ditto 736 void defaultEventHandler_click(ClickEvent event) {} 737 /// ditto 738 void defaultEventHandler_keydown(KeyDownEvent event) {} 739 /// ditto 740 void defaultEventHandler_keyup(KeyUpEvent event) {} 741 /// ditto 742 void defaultEventHandler_mousedown(MouseDownEvent event) {} 743 /// ditto 744 void defaultEventHandler_mouseover(MouseOverEvent event) {} 745 /// ditto 746 void defaultEventHandler_mouseout(MouseOutEvent event) {} 747 /// ditto 748 void defaultEventHandler_mouseup(MouseUpEvent event) {} 749 /// ditto 750 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 751 /// ditto 752 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 753 /// ditto 754 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 755 /// ditto 756 void defaultEventHandler_char(CharEvent event) {} 757 /// ditto 758 void defaultEventHandler_triggered(Event event) {} 759 /// ditto 760 void defaultEventHandler_change(Event event) {} 761 /// ditto 762 void defaultEventHandler_focus(Event event) {} 763 /// ditto 764 void defaultEventHandler_blur(Event event) {} 765 766 /++ 767 [Event]s use a Javascript-esque model. See more details on the [Event] page. 768 769 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 770 771 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 772 of participating in handler delegation. 773 +/ 774 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 775 return addEventListener(event, (Widget, Event e) { 776 if(e.srcElement is this) 777 handler(); 778 }, useCapture); 779 } 780 781 /// ditto 782 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 783 return addEventListener(event, (Widget, Event e) { 784 if(e.srcElement is this) 785 handler(e); 786 }, useCapture); 787 } 788 789 /// ditto 790 @scriptable 791 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 792 return addEventListener(event, (Widget, Event) { handler(); }, useCapture); 793 } 794 795 /// ditto 796 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 797 static if(is(Handler Fn == delegate)) { 798 static if(is(Fn Params == __parameters)) { 799 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 800 auto ty = cast(Params[0]) e; 801 if(ty !is null) 802 handler(ty); 803 }, useCapture); 804 } else static assert(0); 805 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate."); 806 } 807 808 /// ditto 809 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 810 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 811 } 812 813 /// ditto 814 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 815 if(event.length > 2 && event[0..2] == "on") 816 event = event[2 .. $]; 817 818 if(useCapture) 819 capturingEventHandlers[event] ~= handler; 820 else 821 bubblingEventHandlers[event] ~= handler; 822 823 return EventListener(this, event, handler, useCapture); 824 } 825 826 /// ditto 827 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 828 if(event.length > 2 && event[0..2] == "on") 829 event = event[2 .. $]; 830 831 if(useCapture) { 832 if(event in capturingEventHandlers) 833 foreach(ref evt; capturingEventHandlers[event]) 834 if(evt is handler) evt = null; 835 } else { 836 if(event in bubblingEventHandlers) 837 foreach(ref evt; bubblingEventHandlers[event]) 838 if(evt is handler) evt = null; 839 } 840 } 841 842 /// ditto 843 void removeEventListener(EventListener listener) { 844 removeEventListener(listener.event, listener.handler, listener.useCapture); 845 } 846 847 static if(UsingSimpledisplayX11) { 848 void discardXConnectionState() { 849 foreach(child; children) 850 child.discardXConnectionState(); 851 } 852 853 void recreateXConnectionState() { 854 foreach(child; children) 855 child.recreateXConnectionState(); 856 redraw(); 857 } 858 } 859 860 /++ 861 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 862 863 History: 864 `globalCoordinates` was made `final` on May 11, 2021. 865 +/ 866 Point globalCoordinates() { 867 int x = this.x; 868 int y = this.y; 869 auto p = this.parent; 870 while(p) { 871 x += p.x; 872 y += p.y; 873 p = p.parent; 874 } 875 876 static if(UsingSimpledisplayX11) { 877 auto dpy = XDisplayConnection.get; 878 arsd.simpledisplay.Window dummyw; 879 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 880 } else { 881 POINT pt; 882 pt.x = x; 883 pt.y = y; 884 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 885 x = pt.x; 886 y = pt.y; 887 } 888 889 return Point(x, y); 890 } 891 892 version(win32_widgets) 893 void handleWmCommand(ushort cmd, ushort id) {} 894 895 version(win32_widgets) 896 int handleWmNotify(NMHDR* hdr, int code) { return 0; } 897 898 /++ 899 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 900 901 Updates to this variable will only be made visible on the next mouse enter event. 902 +/ 903 @scriptable string statusTip; 904 // string toolTip; 905 // string helpText; 906 907 /++ 908 If true, this widget can be focused via keyboard control with the tab key. 909 910 If false, it is assumed the widget itself does not contain the keyboard focus (though its childen are free to). 911 +/ 912 bool tabStop = true; 913 /++ 914 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.) 915 +/ 916 int tabOrder; 917 918 version(win32_widgets) { 919 static Widget[HWND] nativeMapping; 920 HWND hwnd; 921 WNDPROC originalWindowProcedure; 922 923 SimpleWindow simpleWindowWrappingHwnd; 924 925 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 926 switch(iMessage) { 927 case WM_COMMAND: 928 auto handle = cast(HWND) lParam; 929 auto cmd = HIWORD(wParam); 930 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 931 default: 932 } 933 return 0; 934 } 935 } 936 private bool implicitlyCreated; 937 938 /// 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. 939 int x; 940 /// ditto 941 int y; 942 private int _width; 943 private int _height; 944 private Widget[] _children; 945 private Widget _parent; 946 private Window _parentWindow; 947 948 /++ 949 Returns the window to which this widget is attached. 950 951 History: 952 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. 953 +/ 954 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 955 private @property void parentWindow(Window parent) { 956 _parentWindow = parent; 957 foreach(child; children) 958 child.parentWindow = parent; // please note that this is recursive 959 } 960 961 /++ 962 Returns the list of the widget's children. 963 964 History: 965 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 966 967 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 968 +/ 969 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 970 971 /++ 972 Returns the widget's parent. 973 974 History: 975 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 976 977 The parent should only be managed by the [addChild] and [removeWidget] method. 978 +/ 979 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 980 981 /// The widget's current size. 982 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 983 /// ditto 984 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 985 986 /// Only the layout manager should be calling these. 987 final protected @property int width(int a) @safe { return _width = a; } 988 /// ditto 989 final protected @property int height(int a) @safe { return _height = a; } 990 991 /++ 992 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. 993 994 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 995 +/ 996 protected void registerMovement() { 997 version(win32_widgets) { 998 if(hwnd) { 999 auto pos = getChildPositionRelativeToParentHwnd(this); 1000 MoveWindow(hwnd, pos[0], pos[1], width, height, true); 1001 } 1002 } 1003 sendResizeEvent(); 1004 } 1005 1006 /// Creates the widget and adds it to the parent. 1007 this(Widget parent = null) { 1008 if(parent !is null) 1009 parent.addChild(this); 1010 setupDefaultEventHandlers(); 1011 } 1012 1013 /// 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. 1014 @scriptable 1015 bool isFocused() { 1016 return parentWindow && parentWindow.focusedWidget is this; 1017 } 1018 1019 private bool showing_ = true; 1020 /// 1021 bool showing() { return showing_; } 1022 /// 1023 bool hidden() { return !showing_; } 1024 /++ 1025 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. 1026 +/ 1027 void showing(bool s, bool recalculate = true) { 1028 auto so = showing_; 1029 showing_ = s; 1030 if(s != so) { 1031 1032 version(win32_widgets) 1033 if(hwnd) 1034 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1035 1036 if(parent && recalculate) { 1037 parent.recomputeChildLayout(); 1038 parent.redraw(); 1039 } 1040 1041 foreach(child; children) 1042 child.showing(s, false); 1043 } 1044 } 1045 /// Convenience method for `showing = true` 1046 @scriptable 1047 void show() { 1048 showing = true; 1049 } 1050 /// Convenience method for `showing = false` 1051 @scriptable 1052 void hide() { 1053 showing = false; 1054 } 1055 1056 /// 1057 @scriptable 1058 void focus() { 1059 assert(parentWindow !is null); 1060 if(isFocused()) 1061 return; 1062 1063 if(parentWindow.focusedWidget) { 1064 // FIXME: more details here? like from and to 1065 auto from = parentWindow.focusedWidget; 1066 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1067 parentWindow.focusedWidget = null; 1068 from.emit!BlurEvent(); 1069 } 1070 1071 1072 version(win32_widgets) { 1073 if(this.hwnd !is null) 1074 SetFocus(this.hwnd); 1075 } 1076 1077 parentWindow.focusedWidget = this; 1078 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1079 this.emit!FocusEvent(); 1080 } 1081 1082 1083 /++ 1084 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1085 1086 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1087 +/ 1088 void attachedToWindow(Window w) {} 1089 /++ 1090 Callback when the widget is added to another widget. 1091 1092 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1093 +/ 1094 void addedTo(Widget w) {} 1095 1096 /++ 1097 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. 1098 1099 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1100 +/ 1101 protected void addChild(Widget w, int position = int.max) { 1102 w._parent = this; 1103 if(position == int.max || position == children.length) { 1104 _children ~= w; 1105 } else { 1106 assert(position < _children.length); 1107 _children.length = _children.length + 1; 1108 for(int i = cast(int) _children.length - 1; i > position; i--) 1109 _children[i] = _children[i - 1]; 1110 _children[position] = w; 1111 } 1112 1113 this.parentWindow = this._parentWindow; 1114 1115 w.addedTo(this); 1116 1117 if(this.hidden) 1118 w.showing = false; 1119 1120 if(parentWindow !is null) { 1121 w.attachedToWindow(parentWindow); 1122 parentWindow.recomputeChildLayout(); 1123 parentWindow.redraw(); 1124 } 1125 } 1126 1127 /++ 1128 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. 1129 +/ 1130 Widget getChildAtPosition(int x, int y) { 1131 // it goes backward so the last one to show gets picked first 1132 // might use z-index later 1133 foreach_reverse(child; children) { 1134 if(child.hidden) 1135 continue; 1136 if(child.x <= x && child.y <= y 1137 && ((x - child.x) < child.width) 1138 && ((y - child.y) < child.height)) 1139 { 1140 return child; 1141 } 1142 } 1143 1144 return null; 1145 } 1146 1147 /++ 1148 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. 1149 1150 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1151 +/ 1152 void paint(WidgetPainter painter) {} 1153 1154 deprecated("Change ScreenPainter to WidgetPainter") 1155 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1156 1157 /// I don't actually like the name of this 1158 /// this draws a background on it 1159 void erase(WidgetPainter painter) { 1160 version(win32_widgets) 1161 if(hwnd) return; // Windows will do it. I think. 1162 1163 auto c = getComputedStyle().backgroundColor; 1164 painter.fillColor = c; 1165 painter.outlineColor = c; 1166 1167 version(win32_widgets) { 1168 HANDLE b, p; 1169 if(c.a == 0) { 1170 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1171 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1172 } 1173 } 1174 painter.drawRectangle(Point(0, 0), width, height); 1175 version(win32_widgets) { 1176 if(c.a == 0) { 1177 SelectObject(painter.impl.hdc, p); 1178 SelectObject(painter.impl.hdc, b); 1179 } 1180 } 1181 } 1182 1183 /// 1184 WidgetPainter draw() { 1185 int x = this.x, y = this.y; 1186 auto parent = this.parent; 1187 while(parent) { 1188 x += parent.x; 1189 y += parent.y; 1190 parent = parent.parent; 1191 } 1192 1193 auto painter = parentWindow.win.draw(); 1194 painter.originX = x; 1195 painter.originY = y; 1196 painter.setClipRectangle(Point(0, 0), width, height); 1197 return WidgetPainter(painter, this); 1198 } 1199 1200 /// 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. 1201 protected void privatePaint(WidgetPainter painter, int lox, int loy, bool force = false) { 1202 if(hidden) 1203 return; 1204 1205 painter.originX = lox + x; 1206 painter.originY = loy + y; 1207 1208 bool actuallyPainted = false; 1209 1210 if(redrawRequested || force) { 1211 painter.setClipRectangle(Point(0, 0), width, height); 1212 1213 painter.drawingUpon = this; 1214 1215 erase(painter); 1216 if(painter.visualTheme) 1217 painter.visualTheme.doPaint(this, painter); 1218 else 1219 paint(painter); 1220 1221 redrawRequested = false; 1222 actuallyPainted = true; 1223 } 1224 1225 foreach(child; children) { 1226 version(win32_widgets) 1227 if(child.useNativeDrawing()) continue; 1228 child.privatePaint(painter, painter.originX, painter.originY, actuallyPainted); 1229 } 1230 1231 version(win32_widgets) 1232 foreach(child; children) { 1233 if(child.useNativeDrawing) { 1234 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw, child); 1235 child.privatePaint(painter, painter.originX, painter.originY, actuallyPainted); 1236 } 1237 } 1238 } 1239 1240 protected bool useNativeDrawing() nothrow { 1241 version(win32_widgets) 1242 return hwnd !is null; 1243 else 1244 return false; 1245 } 1246 1247 private static class RedrawEvent {} 1248 private __gshared re = new RedrawEvent(); 1249 1250 private bool redrawRequested; 1251 /// 1252 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1253 redrawRequested = true; 1254 1255 if(this.parentWindow) { 1256 auto sw = this.parentWindow.win; 1257 assert(sw !is null); 1258 if(!sw.eventQueued!RedrawEvent) { 1259 sw.postEvent(re); 1260 //import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1261 } 1262 } 1263 } 1264 1265 private void actualRedraw() { 1266 if(!showing) return; 1267 1268 assert(parentWindow !is null); 1269 1270 auto w = drawableWindow; 1271 if(w is null) 1272 w = parentWindow.win; 1273 1274 if(w.closed()) 1275 return; 1276 1277 auto ugh = this.parent; 1278 int lox, loy; 1279 while(ugh) { 1280 lox += ugh.x; 1281 loy += ugh.y; 1282 ugh = ugh.parent; 1283 } 1284 auto painter = w.draw(); 1285 privatePaint(WidgetPainter(painter, this), lox, loy); 1286 } 1287 1288 private SimpleWindow drawableWindow; 1289 1290 /++ 1291 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. 1292 1293 Returns: 1294 `true` if you should do your default behavior. 1295 1296 History: 1297 Added May 5, 2021 1298 +/ 1299 final protected bool emit(EventType, this This, Args...)(Args args) { 1300 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1301 auto e = new EventType(this, args); 1302 e.dispatch(); 1303 return !e.defaultPrevented; 1304 } 1305 /// ditto 1306 final protected bool emit(string eventString, this This)() { 1307 auto e = new Event(eventString, this); 1308 e.dispatch(); 1309 return !e.defaultPrevented; 1310 } 1311 1312 /++ 1313 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. 1314 1315 History: 1316 Added May 5, 2021 1317 +/ 1318 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1319 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1320 return addEventListener(handler); 1321 } 1322 1323 /++ 1324 Gets the computed style properties from the visual theme. 1325 1326 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].) 1327 1328 History: 1329 Added May 8, 2021 1330 +/ 1331 final StyleInformation getComputedStyle() { 1332 return StyleInformation(this); 1333 } 1334 1335 // FIXME: I kinda want to hide events from implementation widgets 1336 // so it just catches them all and stops propagation... 1337 // i guess i can do it with a event listener on star. 1338 1339 mixin Emits!KeyDownEvent; /// 1340 mixin Emits!KeyUpEvent; /// 1341 mixin Emits!CharEvent; /// 1342 1343 mixin Emits!MouseDownEvent; /// 1344 mixin Emits!MouseUpEvent; /// 1345 mixin Emits!ClickEvent; /// 1346 mixin Emits!DoubleClickEvent; /// 1347 mixin Emits!MouseMoveEvent; /// 1348 mixin Emits!MouseOverEvent; /// 1349 mixin Emits!MouseOutEvent; /// 1350 mixin Emits!MouseEnterEvent; /// 1351 mixin Emits!MouseLeaveEvent; /// 1352 1353 mixin Emits!ResizeEvent; /// 1354 1355 mixin Emits!BlurEvent; /// 1356 mixin Emits!FocusEvent; /// 1357 } 1358 1359 /// 1360 abstract class ComboboxBase : Widget { 1361 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 1362 // or to always show the list, we want CBS_SIMPLE == 1 1363 version(win32_widgets) 1364 this(uint style, Widget parent = null) { 1365 super(parent); 1366 createWin32Window(this, "ComboBox"w, null, style); 1367 } 1368 else version(custom_widgets) 1369 this(Widget parent = null) { 1370 super(parent); 1371 1372 addEventListener((KeyDownEvent event) { 1373 if(event.key == Key.Up) { 1374 if(selection > -1) { // -1 means select blank 1375 selection--; 1376 fireChangeEvent(); 1377 } 1378 event.preventDefault(); 1379 } 1380 if(event.key == Key.Down) { 1381 if(selection + 1 < options.length) { 1382 selection++; 1383 fireChangeEvent(); 1384 } 1385 event.preventDefault(); 1386 } 1387 1388 }); 1389 1390 } 1391 else static assert(false); 1392 1393 private string[] options; 1394 private int selection = -1; 1395 1396 void addOption(string s) { 1397 options ~= s; 1398 version(win32_widgets) 1399 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 1400 } 1401 1402 void setSelection(int idx) { 1403 selection = idx; 1404 version(win32_widgets) 1405 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 1406 1407 auto t = new SelectionChangedEvent(this, selection, selection == -1 ? null : options[selection]); 1408 t.dispatch(); 1409 } 1410 1411 static class SelectionChangedEvent : Event { 1412 this(Widget target, int iv, string sv) { 1413 super("change", target); 1414 this.iv = iv; 1415 this.sv = sv; 1416 } 1417 immutable int iv; 1418 immutable string sv; 1419 1420 override @property string stringValue() { return sv; } 1421 override @property int intValue() { return iv; } 1422 } 1423 1424 version(win32_widgets) 1425 override void handleWmCommand(ushort cmd, ushort id) { 1426 selection = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 1427 fireChangeEvent(); 1428 } 1429 1430 private void fireChangeEvent() { 1431 if(selection >= options.length) 1432 selection = -1; 1433 1434 auto t = new SelectionChangedEvent(this, selection, selection == -1 ? null : options[selection]); 1435 t.dispatch(); 1436 } 1437 1438 version(win32_widgets) { 1439 override int minHeight() { return Window.lineHeight + 6; } 1440 override int maxHeight() { return Window.lineHeight + 6; } 1441 } else { 1442 override int minHeight() { return Window.lineHeight + 4; } 1443 override int maxHeight() { return Window.lineHeight + 4; } 1444 } 1445 1446 version(custom_widgets) { 1447 SimpleWindow dropDown; 1448 void popup() { 1449 auto w = width; 1450 // FIXME: suggestedDropdownHeight see below 1451 auto h = cast(int) this.options.length * Window.lineHeight + 8; 1452 1453 auto coord = this.globalCoordinates(); 1454 auto dropDown = new SimpleWindow( 1455 w, h, 1456 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 1457 1458 dropDown.move(coord.x, coord.y + this.height); 1459 1460 { 1461 auto cs = getComputedStyle(); 1462 auto painter = dropDown.draw(); 1463 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().backgroundColor); 1464 auto p = Point(4, 4); 1465 painter.outlineColor = cs.foregroundColor; 1466 foreach(option; options) { 1467 painter.drawText(p, option); 1468 p.y += Window.lineHeight; 1469 } 1470 } 1471 1472 dropDown.setEventHandlers( 1473 (MouseEvent event) { 1474 if(event.type == MouseEventType.buttonReleased) { 1475 auto element = (event.y - 4) / Window.lineHeight; 1476 if(element >= 0 && element <= options.length) { 1477 selection = element; 1478 1479 fireChangeEvent(); 1480 } 1481 dropDown.close(); 1482 } 1483 } 1484 ); 1485 1486 dropDown.show(); 1487 dropDown.grabInput(); 1488 } 1489 1490 } 1491 } 1492 1493 /++ 1494 A drop-down list where the user must select one of the 1495 given options. Like `<select>` in HTML. 1496 +/ 1497 class DropDownSelection : ComboboxBase { 1498 this(Widget parent = null) { 1499 version(win32_widgets) 1500 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 1501 else version(custom_widgets) { 1502 super(parent); 1503 1504 addEventListener("focus", () { this.redraw; }); 1505 addEventListener("blur", () { this.redraw; }); 1506 addEventListener(EventType.change, () { this.redraw; }); 1507 addEventListener("mousedown", () { this.focus(); this.popup(); }); 1508 addEventListener((KeyDownEvent event) { 1509 if(event.key == Key.Space) 1510 popup(); 1511 }); 1512 } else static assert(false); 1513 } 1514 1515 version(custom_widgets) 1516 override void paint(WidgetPainter painter) { 1517 auto cs = getComputedStyle(); 1518 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle.backgroundColor); 1519 painter.outlineColor = cs.foregroundColor; 1520 painter.drawText(Point(4, 4), selection == -1 ? "" : options[selection]); 1521 1522 painter.outlineColor = cs.foregroundColor; 1523 painter.fillColor = cs.foregroundColor; 1524 Point[4] triangle; 1525 enum padding = 6; 1526 enum paddingV = 7; 1527 enum triangleWidth = 10; 1528 triangle[0] = Point(width - padding - triangleWidth, paddingV); 1529 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 1530 triangle[2] = Point(width - padding - 0, paddingV); 1531 triangle[3] = triangle[0]; 1532 painter.drawPolygon(triangle[]); 1533 1534 if(isFocused()) { 1535 painter.fillColor = Color.transparent; 1536 painter.pen = Pen(cs.foregroundColor, 1, Pen.Style.Dotted); 1537 painter.drawRectangle(Point(2, 2), width - 4, height - 4); 1538 painter.pen = Pen(cs.foregroundColor, 1, Pen.Style.Solid); 1539 1540 } 1541 } 1542 1543 version(win32_widgets) 1544 override void registerMovement() { 1545 version(win32_widgets) { 1546 if(hwnd) { 1547 auto pos = getChildPositionRelativeToParentHwnd(this); 1548 // the height given to this from Windows' perspective is supposed 1549 // to include the drop down's height. so I add to it to give some 1550 // room for that. 1551 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 1552 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 1553 } 1554 } 1555 sendResizeEvent(); 1556 } 1557 } 1558 1559 /++ 1560 A text box with a drop down arrow listing selections. 1561 The user can choose from the list, or type their own. 1562 +/ 1563 class FreeEntrySelection : ComboboxBase { 1564 this(Widget parent = null) { 1565 version(win32_widgets) 1566 super(2 /* CBS_DROPDOWN */, parent); 1567 else version(custom_widgets) { 1568 super(parent); 1569 auto hl = new HorizontalLayout(this); 1570 lineEdit = new LineEdit(hl); 1571 1572 tabStop = false; 1573 1574 lineEdit.addEventListener("focus", &lineEdit.selectAll); 1575 1576 auto btn = new class ArrowButton { 1577 this() { 1578 super(ArrowDirection.down, hl); 1579 } 1580 override int maxHeight() { 1581 return int.max; 1582 } 1583 }; 1584 //btn.addDirectEventListener("focus", &lineEdit.focus); 1585 btn.addEventListener("triggered", &this.popup); 1586 addEventListener(EventType.change, (Event event) { 1587 lineEdit.content = event.stringValue; 1588 lineEdit.focus(); 1589 redraw(); 1590 }); 1591 } 1592 else static assert(false); 1593 } 1594 1595 version(custom_widgets) { 1596 LineEdit lineEdit; 1597 } 1598 } 1599 1600 /++ 1601 A combination of free entry with a list below it. 1602 +/ 1603 class ComboBox : ComboboxBase { 1604 this(Widget parent = null) { 1605 version(win32_widgets) 1606 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 1607 else version(custom_widgets) { 1608 super(parent); 1609 lineEdit = new LineEdit(this); 1610 listWidget = new ListWidget(this); 1611 listWidget.multiSelect = false; 1612 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 1613 string c = null; 1614 foreach(option; listWidget.options) 1615 if(option.selected) { 1616 c = option.label; 1617 break; 1618 } 1619 lineEdit.content = c; 1620 }); 1621 1622 listWidget.tabStop = false; 1623 this.tabStop = false; 1624 listWidget.addEventListener("focus", &lineEdit.focus); 1625 this.addEventListener("focus", &lineEdit.focus); 1626 1627 addDirectEventListener(EventType.change, { 1628 listWidget.setSelection(selection); 1629 if(selection != -1) 1630 lineEdit.content = options[selection]; 1631 lineEdit.focus(); 1632 redraw(); 1633 }); 1634 1635 lineEdit.addEventListener("focus", &lineEdit.selectAll); 1636 1637 listWidget.addDirectEventListener(EventType.change, { 1638 int set = -1; 1639 foreach(idx, opt; listWidget.options) 1640 if(opt.selected) { 1641 set = cast(int) idx; 1642 break; 1643 } 1644 if(set != selection) 1645 this.setSelection(set); 1646 }); 1647 } else static assert(false); 1648 } 1649 1650 override int minHeight() { return Window.lineHeight * 3; } 1651 override int maxHeight() { return int.max; } 1652 override int heightStretchiness() { return 5; } 1653 1654 version(custom_widgets) { 1655 LineEdit lineEdit; 1656 ListWidget listWidget; 1657 1658 override void addOption(string s) { 1659 listWidget.options ~= ListWidget.Option(s); 1660 ComboboxBase.addOption(s); 1661 } 1662 } 1663 } 1664 1665 /+ 1666 class Spinner : Widget { 1667 version(win32_widgets) 1668 this(Widget parent = null) { 1669 super(parent); 1670 parentWindow = parent.parentWindow; 1671 auto hlayout = new HorizontalLayout(this); 1672 lineEdit = new LineEdit(hlayout); 1673 upDownControl = new UpDownControl(hlayout); 1674 } 1675 1676 LineEdit lineEdit; 1677 UpDownControl upDownControl; 1678 } 1679 1680 class UpDownControl : Widget { 1681 version(win32_widgets) 1682 this(Widget parent = null) { 1683 super(parent); 1684 parentWindow = parent.parentWindow; 1685 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 1686 } 1687 1688 override int minHeight() { return Window.lineHeight; } 1689 override int maxHeight() { return Window.lineHeight * 3/2; } 1690 1691 override int minWidth() { return Window.lineHeight * 3/2; } 1692 override int maxWidth() { return Window.lineHeight * 3/2; } 1693 } 1694 +/ 1695 1696 /+ 1697 class DataView : Widget { 1698 // this is the omnibus data viewer 1699 // the internal data layout is something like: 1700 // string[string][] but also each node can have parents 1701 } 1702 +/ 1703 1704 1705 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 1706 1707 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 1708 1709 // FIXME: menus should prolly capture the mouse. ugh i kno. 1710 /* 1711 TextEdit needs: 1712 1713 * caret manipulation 1714 * selection control 1715 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 1716 1717 For example: 1718 1719 connect(paste, &textEdit.insertTextAtCaret); 1720 1721 would be nice. 1722 1723 1724 1725 I kinda want an omnibus dataview that combines list, tree, 1726 and table - it can be switched dynamically between them. 1727 1728 Flattening policy: only show top level, show recursive, show grouped 1729 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 1730 1731 Single select, multi select, organization, drag+drop 1732 */ 1733 1734 //static if(UsingSimpledisplayX11) 1735 version(win32_widgets) {} 1736 else version(custom_widgets) { 1737 enum scrollClickRepeatInterval = 50; 1738 1739 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 1740 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 1741 enum activeTabColor = lightAccentColor; 1742 enum hoveringColor = Color(228, 228, 228); 1743 enum buttonColor = windowBackgroundColor; 1744 enum depressedButtonColor = darkAccentColor; 1745 enum activeListXorColor = Color(255, 255, 127); 1746 enum progressBarColor = Color(0, 0, 128); 1747 enum activeMenuItemColor = Color(0, 0, 128); 1748 1749 }} 1750 else static assert(false); 1751 deprecated("Get these properties off the `visualTheme` instead.") { 1752 // these are used by horizontal rule so not just custom_widgets. for now at least. 1753 enum darkAccentColor = Color(172, 172, 172); 1754 enum lightAccentColor = Color(223, 223, 223); // used to be 223 1755 } 1756 1757 private const(wchar)* toWstringzInternal(in char[] s) { 1758 wchar[] str; 1759 str.reserve(s.length + 1); 1760 foreach(dchar ch; s) 1761 str ~= ch; 1762 str ~= '\0'; 1763 return str.ptr; 1764 } 1765 1766 static if(SimpledisplayTimerAvailable) 1767 void setClickRepeat(Widget w, int interval, int delay = 250) { 1768 Timer timer; 1769 int delayRemaining = delay / interval; 1770 if(delayRemaining <= 1) 1771 delayRemaining = 2; 1772 1773 immutable originalDelayRemaining = delayRemaining; 1774 1775 w.addDirectEventListener("mousedown", (Event ev) { 1776 if(ev.srcElement !is w) 1777 return; 1778 if(timer !is null) { 1779 timer.destroy(); 1780 timer = null; 1781 } 1782 delayRemaining = originalDelayRemaining; 1783 timer = new Timer(interval, () { 1784 if(delayRemaining > 0) 1785 delayRemaining--; 1786 else { 1787 auto ev = new ClickEvent(w); 1788 ev.sendDirectly(); 1789 } 1790 }); 1791 }); 1792 1793 w.addDirectEventListener("mouseup", (Event ev) { 1794 if(ev.srcElement !is w) 1795 return; 1796 if(timer !is null) { 1797 timer.destroy(); 1798 timer = null; 1799 } 1800 }); 1801 1802 w.addDirectEventListener("mouseleave", (Event ev) { 1803 if(ev.srcElement !is w) 1804 return; 1805 if(timer !is null) { 1806 timer.destroy(); 1807 timer = null; 1808 } 1809 }); 1810 1811 } 1812 else 1813 void setClickRepeat(Widget w, int interval, int delay = 250) {} 1814 1815 enum FrameStyle { 1816 none, /// 1817 risen, /// a 3d pop-out effect (think Windows 95 button) 1818 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 1819 solid, /// 1820 dotted, /// 1821 fantasy, /// a style based on a popular fantasy video game 1822 } 1823 1824 version(custom_widgets) 1825 deprecated 1826 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 1827 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 1828 } 1829 1830 version(custom_widgets) 1831 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 1832 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 1833 } 1834 1835 version(custom_widgets) 1836 deprecated 1837 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 1838 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 1839 } 1840 1841 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 1842 int borderWidth; 1843 final switch(style) { 1844 case FrameStyle.sunk, FrameStyle.risen: 1845 // outer layer 1846 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 1847 borderWidth = 2; 1848 break; 1849 case FrameStyle.none: 1850 painter.outlineColor = background; 1851 borderWidth = 0; 1852 break; 1853 case FrameStyle.solid: 1854 painter.pen = Pen(border, 1); 1855 borderWidth = 1; 1856 break; 1857 case FrameStyle.dotted: 1858 painter.pen = Pen(border, 1, Pen.Style.Dotted); 1859 borderWidth = 1; 1860 break; 1861 case FrameStyle.fantasy: 1862 painter.pen = Pen(border, 3); 1863 borderWidth = 3; 1864 break; 1865 } 1866 1867 painter.fillColor = background; 1868 painter.drawRectangle(Point(x + 0, y + 0), width, height); 1869 1870 1871 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 1872 // 3d effect 1873 auto vt = WidgetPainter.visualTheme; 1874 1875 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 1876 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 1877 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 1878 1879 // inner layer 1880 //right, bottom 1881 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 1882 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 1883 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 1884 // left, top 1885 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 1886 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 1887 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 1888 } else if(style == FrameStyle.fantasy) { 1889 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 1890 painter.fillColor = Color.transparent; 1891 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 1892 } 1893 1894 return borderWidth; 1895 } 1896 1897 /// 1898 class Action { 1899 version(win32_widgets) { 1900 private int id; 1901 private static int lastId = 9000; 1902 private static Action[int] mapping; 1903 } 1904 1905 KeyEvent accelerator; 1906 1907 /// 1908 this(string label, ushort icon = 0, void delegate() triggered = null) { 1909 this.label = label; 1910 this.iconId = icon; 1911 if(triggered !is null) 1912 this.triggered ~= triggered; 1913 version(win32_widgets) { 1914 id = ++lastId; 1915 mapping[id] = this; 1916 } 1917 } 1918 1919 private string label; 1920 private ushort iconId; 1921 // icon 1922 1923 // when it is triggered, the triggered event is fired on the window 1924 void delegate()[] triggered; 1925 } 1926 1927 /* 1928 plan: 1929 keyboard accelerators 1930 1931 * menus (and popups and tooltips) 1932 * status bar 1933 * toolbars and buttons 1934 1935 sortable table view 1936 1937 maybe notification area icons 1938 basic clipboard 1939 1940 * radio box 1941 splitter 1942 toggle buttons (optionally mutually exclusive, like in Paint) 1943 label, rich text display, multi line plain text (selectable) 1944 * fieldset 1945 * nestable grid layout 1946 single line text input 1947 * multi line text input 1948 slider 1949 spinner 1950 list box 1951 drop down 1952 combo box 1953 auto complete box 1954 * progress bar 1955 1956 terminal window/widget (on unix it might even be a pty but really idk) 1957 1958 ok button 1959 cancel button 1960 1961 keyboard hotkeys 1962 1963 scroll widget 1964 1965 event redirections and network transparency 1966 script integration 1967 */ 1968 1969 1970 /* 1971 MENUS 1972 1973 auto bar = new MenuBar(window); 1974 window.menuBar = bar; 1975 1976 auto fileMenu = bar.addItem(new Menu("&File")); 1977 fileMenu.addItem(new MenuItem("&Exit")); 1978 1979 1980 EVENTS 1981 1982 For controls, you should usually use "triggered" rather than "click", etc., because 1983 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 1984 This is the case on menus and pushbuttons. 1985 1986 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 1987 */ 1988 1989 1990 /* 1991 enum LinePreference { 1992 AlwaysOnOwnLine, // always on its own line 1993 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 1994 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 1995 } 1996 */ 1997 1998 mixin template Padding(string code) { 1999 override int paddingLeft() { return mixin(code);} 2000 override int paddingRight() { return mixin(code);} 2001 override int paddingTop() { return mixin(code);} 2002 override int paddingBottom() { return mixin(code);} 2003 } 2004 2005 mixin template Margin(string code) { 2006 override int marginLeft() { return mixin(code);} 2007 override int marginRight() { return mixin(code);} 2008 override int marginTop() { return mixin(code);} 2009 override int marginBottom() { return mixin(code);} 2010 } 2011 2012 private 2013 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 2014 enum calcingV = relevantMeasure == "height"; 2015 2016 parent.registerMovement(); 2017 2018 if(parent.children.length == 0) 2019 return; 2020 2021 auto parentStyle = parent.getComputedStyle(); 2022 2023 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 2024 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 2025 2026 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 2027 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 2028 2029 // my own width and height should already be set by the caller of this function... 2030 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 2031 mixin("parentStyle.padding"~firstThingy~"()") - 2032 mixin("parentStyle.padding"~secondThingy~"()"); 2033 2034 int stretchinessSum; 2035 int stretchyChildSum; 2036 int lastMargin = 0; 2037 2038 // set initial size 2039 foreach(child; parent.children) { 2040 2041 auto childStyle = child.getComputedStyle(); 2042 2043 if(cast(StaticPosition) child) 2044 continue; 2045 if(child.hidden) 2046 continue; 2047 2048 static if(calcingV) { 2049 child.width = parent.width - 2050 mixin("childStyle.margin"~otherFirstThingy~"()") - 2051 mixin("childStyle.margin"~otherSecondThingy~"()") - 2052 mixin("parentStyle.padding"~otherFirstThingy~"()") - 2053 mixin("parentStyle.padding"~otherSecondThingy~"()"); 2054 2055 if(child.width < 0) 2056 child.width = 0; 2057 if(child.width > childStyle.maxWidth()) 2058 child.width = childStyle.maxWidth(); 2059 child.height = childStyle.minHeight(); 2060 } else { 2061 child.height = parent.height - 2062 mixin("childStyle.margin"~firstThingy~"()") - 2063 mixin("childStyle.margin"~secondThingy~"()") - 2064 mixin("parentStyle.padding"~firstThingy~"()") - 2065 mixin("parentStyle.padding"~secondThingy~"()"); 2066 if(child.height < 0) 2067 child.height = 0; 2068 if(child.height > childStyle.maxHeight()) 2069 child.height = childStyle.maxHeight(); 2070 child.width = childStyle.minWidth(); 2071 } 2072 2073 spaceRemaining -= mixin("child." ~ relevantMeasure); 2074 2075 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 2076 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 2077 lastMargin = margin; 2078 spaceRemaining -= thisMargin + margin; 2079 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 2080 stretchinessSum += s; 2081 if(s > 0) 2082 stretchyChildSum++; 2083 } 2084 2085 // stretch to fill space 2086 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 2087 //import std.stdio; writeln("str ", stretchinessSum); 2088 auto spacePerChild = spaceRemaining / stretchinessSum; 2089 bool spreadEvenly; 2090 bool giveToBiggest; 2091 if(spacePerChild <= 0) { 2092 spacePerChild = spaceRemaining / stretchyChildSum; 2093 spreadEvenly = true; 2094 } 2095 if(spacePerChild <= 0) { 2096 giveToBiggest = true; 2097 } 2098 int previousSpaceRemaining = spaceRemaining; 2099 stretchinessSum = 0; 2100 Widget mostStretchy; 2101 int mostStretchyS; 2102 foreach(child; parent.children) { 2103 auto childStyle = child.getComputedStyle(); 2104 if(cast(StaticPosition) child) 2105 continue; 2106 if(child.hidden) 2107 continue; 2108 static if(calcingV) 2109 auto maximum = childStyle.maxHeight(); 2110 else 2111 auto maximum = childStyle.maxWidth(); 2112 2113 if(mixin("child." ~ relevantMeasure) >= maximum) { 2114 auto adj = mixin("child." ~ relevantMeasure) - maximum; 2115 mixin("child._" ~ relevantMeasure) -= adj; 2116 spaceRemaining += adj; 2117 continue; 2118 } 2119 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 2120 if(s <= 0) 2121 continue; 2122 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 2123 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 2124 spaceRemaining -= spaceAdjustment; 2125 if(mixin("child." ~ relevantMeasure) > maximum) { 2126 auto diff = mixin("child." ~ relevantMeasure) - maximum; 2127 mixin("child._" ~ relevantMeasure) -= diff; 2128 spaceRemaining += diff; 2129 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 2130 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 2131 if(mostStretchy is null || s >= mostStretchyS) { 2132 mostStretchy = child; 2133 mostStretchyS = s; 2134 } 2135 } 2136 } 2137 2138 if(giveToBiggest && mostStretchy !is null) { 2139 auto child = mostStretchy; 2140 auto childStyle = child.getComputedStyle(); 2141 int spaceAdjustment = spaceRemaining; 2142 2143 static if(calcingV) 2144 auto maximum = childStyle.maxHeight(); 2145 else 2146 auto maximum = childStyle.maxWidth(); 2147 2148 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 2149 spaceRemaining -= spaceAdjustment; 2150 if(mixin("child._" ~ relevantMeasure) > maximum) { 2151 auto diff = mixin("child." ~ relevantMeasure) - maximum; 2152 mixin("child._" ~ relevantMeasure) -= diff; 2153 spaceRemaining += diff; 2154 } 2155 } 2156 2157 if(spaceRemaining == previousSpaceRemaining) 2158 break; // apparently nothing more we can do 2159 } 2160 2161 // position 2162 lastMargin = 0; 2163 int currentPos = mixin("parent.padding"~firstThingy~"()"); 2164 foreach(child; parent.children) { 2165 auto childStyle = child.getComputedStyle(); 2166 if(cast(StaticPosition) child) { 2167 child.recomputeChildLayout(); 2168 continue; 2169 } 2170 if(child.hidden) 2171 continue; 2172 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 2173 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 2174 currentPos += thisMargin; 2175 static if(calcingV) { 2176 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 2177 child.y = currentPos; 2178 } else { 2179 child.x = currentPos; 2180 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 2181 2182 } 2183 currentPos += mixin("child." ~ relevantMeasure); 2184 currentPos += margin; 2185 lastMargin = margin; 2186 2187 child.recomputeChildLayout(); 2188 } 2189 } 2190 2191 int mymax(int a, int b) { return a > b ? a : b; } 2192 2193 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 2194 // and here, it must be integrable with the layout, the event system, and not be painted over. 2195 version(win32_widgets) { 2196 extern(Windows) 2197 private 2198 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 2199 //import std.stdio; try { writeln(iMessage); } catch(Exception e) {}; 2200 if(auto te = hWnd in Widget.nativeMapping) { 2201 try { 2202 2203 te.hookedWndProc(iMessage, wParam, lParam); 2204 2205 if(iMessage == WM_SETFOCUS) { 2206 auto lol = *te; 2207 while(lol !is null && lol.implicitlyCreated) 2208 lol = lol.parent; 2209 lol.focus(); 2210 //(*te).parentWindow.focusedWidget = lol; 2211 } 2212 2213 2214 2215 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 2216 SetBkMode(cast(HDC) wParam, TRANSPARENT); 2217 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 2218 //GetStockObject(NULL_BRUSH); 2219 } 2220 2221 2222 auto pos = getChildPositionRelativeToParentOrigin(*te); 2223 lastDefaultPrevented = false; 2224 // try {import std.stdio; writeln(typeid(*te)); } catch(Exception e) {} 2225 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 2226 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 2227 else { 2228 // it was something we recognized, should only call the window procedure if the default was not prevented 2229 } 2230 } catch(Exception e) { 2231 assert(0, e.toString()); 2232 } 2233 return 0; 2234 } 2235 assert(0, "shouldn't be receiving messages for this window...."); 2236 //import std.conv; 2237 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 2238 } 2239 2240 extern(Windows) 2241 private 2242 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 2243 if(iMessage == WM_ERASEBKGND) { 2244 auto dc = GetDC(hWnd); 2245 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 2246 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 2247 RECT r; 2248 GetWindowRect(hWnd, &r); 2249 // since the pen is null, to fill the whole space, we need the +1 on both. 2250 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 2251 SelectObject(dc, p); 2252 SelectObject(dc, b); 2253 ReleaseDC(hWnd, dc); 2254 return 1; 2255 } 2256 return HookedWndProc(hWnd, iMessage, wParam, lParam); 2257 } 2258 2259 // className MUST be a string literal 2260 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 2261 assert(p.parentWindow !is null); 2262 assert(p.parentWindow.win.impl.hwnd !is null); 2263 2264 auto bsgroupbox = style == BS_GROUPBOX; 2265 2266 HWND phwnd; 2267 2268 auto wtf = p.parent; 2269 while(wtf) { 2270 if(wtf.hwnd !is null) { 2271 phwnd = wtf.hwnd; 2272 break; 2273 } 2274 wtf = wtf.parent; 2275 } 2276 2277 if(phwnd is null) 2278 phwnd = p.parentWindow.win.impl.hwnd; 2279 2280 assert(phwnd !is null); 2281 2282 WCharzBuffer wt = WCharzBuffer(windowText); 2283 2284 style |= WS_VISIBLE | WS_CHILD; 2285 //if(className != WC_TABCONTROL) 2286 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 2287 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 2288 CW_USEDEFAULT, CW_USEDEFAULT, 100, 100, 2289 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 2290 2291 assert(p.hwnd !is null); 2292 2293 2294 static HFONT font; 2295 if(font is null) { 2296 NONCLIENTMETRICS params; 2297 params.cbSize = params.sizeof; 2298 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 2299 font = CreateFontIndirect(¶ms.lfMessageFont); 2300 } 2301 } 2302 2303 if(font) 2304 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 2305 2306 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 2307 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 2308 Widget.nativeMapping[p.hwnd] = p; 2309 2310 if(bsgroupbox) 2311 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 2312 else 2313 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 2314 2315 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 2316 2317 p.registerMovement(); 2318 } 2319 } 2320 2321 version(win32_widgets) 2322 private 2323 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 2324 if(hwnd is null || hwnd in Widget.nativeMapping) 2325 return true; 2326 auto parent = cast(Widget) cast(void*) lparam; 2327 Widget p = new Widget(); 2328 p._parent = parent; 2329 p.parentWindow = parent.parentWindow; 2330 p.hwnd = hwnd; 2331 p.implicitlyCreated = true; 2332 Widget.nativeMapping[p.hwnd] = p; 2333 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 2334 return true; 2335 } 2336 2337 /++ 2338 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 2339 +/ 2340 struct WidgetPainter { 2341 this(ScreenPainter screenPainter, Widget drawingUpon) { 2342 this.drawingUpon = drawingUpon; 2343 this.screenPainter = screenPainter; 2344 if(auto font = visualTheme.defaultFontCached) 2345 this.screenPainter.setFont(font); 2346 } 2347 2348 /// 2349 ScreenPainter screenPainter; 2350 /// Forward to the screen painter for other methods 2351 alias screenPainter this; 2352 2353 private Widget drawingUpon; 2354 2355 /++ 2356 This is the list of rectangles that actually need to be redrawn. 2357 2358 Not actually implemented yet. 2359 +/ 2360 Rectangle[] invalidatedRectangles; 2361 2362 private static BaseVisualTheme _visualTheme; 2363 2364 /++ 2365 Functions to access the visual theme and helpers to easily use it. 2366 2367 These are aware of the current widget's computed style out of the theme. 2368 +/ 2369 static @property BaseVisualTheme visualTheme() { 2370 if(_visualTheme is null) 2371 _visualTheme = new DefaultVisualTheme(); 2372 return _visualTheme; 2373 } 2374 2375 /// ditto 2376 static @property void visualTheme(BaseVisualTheme theme) { 2377 _visualTheme = theme; 2378 } 2379 2380 /// ditto 2381 Color themeForeground() { 2382 return drawingUpon.getComputedStyle().foregroundColor(); 2383 } 2384 2385 /// ditto 2386 Color themeBackground() { 2387 return drawingUpon.getComputedStyle().backgroundColor(); 2388 } 2389 2390 int isDarkTheme() { 2391 return 0; // unspecified, yes, no as enum. FIXME 2392 } 2393 2394 /++ 2395 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. 2396 2397 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 2398 2399 If you change teh clip rectangle, you should change it back before you return. 2400 2401 2402 The sequence it uses is: 2403 background 2404 content (delegated to you) 2405 border 2406 focused outline 2407 selected overlay 2408 2409 Example code: 2410 2411 --- 2412 void paint(WidgetPainter painter) { 2413 painter.drawThemed((bounds) { 2414 return bounds; // if the selection overlay should be contained, you can return it here. 2415 }); 2416 } 2417 --- 2418 +/ 2419 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 2420 drawThemed((WidgetPainter painter, const Rectangle bounds) { 2421 return drawBody(bounds); 2422 }); 2423 } 2424 // this overload is actually mroe for setting the delegate to a virtual function 2425 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 2426 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 2427 2428 auto cs = drawingUpon.getComputedStyle(); 2429 2430 auto bg = cs.backgroundColor; 2431 2432 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 2433 2434 rect.left += borderWidth; 2435 rect.right -= borderWidth; 2436 rect.top += borderWidth; 2437 rect.bottom -= borderWidth; 2438 2439 auto insideBorderRect = rect; 2440 2441 rect.left += cs.paddingLeft; 2442 rect.right -= cs.paddingRight; 2443 rect.top += cs.paddingTop; 2444 rect.bottom += cs.paddingBottom; 2445 2446 this.outlineColor = this.themeForeground; 2447 this.fillColor = bg; 2448 2449 rect = drawBody(this, rect); 2450 2451 if(auto os = cs.outlineStyle()) { 2452 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 2453 this.fillColor = Color.transparent; 2454 this.drawRectangle(insideBorderRect); 2455 } 2456 } 2457 2458 /++ 2459 First, draw the background. 2460 Then draw your content. 2461 Next, draw the border. 2462 And the focused indicator. 2463 And the is-selected box. 2464 2465 If it is focused i can draw the outline too... 2466 2467 If selected i can even do the xor action but that's at the end. 2468 +/ 2469 void drawThemeBackground() { 2470 2471 } 2472 2473 void drawThemeBorder() { 2474 2475 } 2476 2477 // all this stuff is a dangerous experiment.... 2478 static class ScriptableVersion { 2479 ScreenPainterImplementation* p; 2480 int originX, originY; 2481 2482 @scriptable: 2483 void drawRectangle(int x, int y, int width, int height) { 2484 p.drawRectangle(x + originX, y + originY, width, height); 2485 } 2486 void drawLine(int x1, int y1, int x2, int y2) { 2487 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 2488 } 2489 void drawText(int x, int y, string text) { 2490 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 2491 } 2492 void setOutlineColor(int r, int g, int b) { 2493 p.pen = Pen(Color(r,g,b), 1); 2494 } 2495 void setFillColor(int r, int g, int b) { 2496 p.fillColor = Color(r,g,b); 2497 } 2498 } 2499 2500 ScriptableVersion toArsdJsvar() { 2501 auto sv = new ScriptableVersion; 2502 sv.p = this.screenPainter.impl; 2503 sv.originX = this.screenPainter.originX; 2504 sv.originY = this.screenPainter.originY; 2505 return sv; 2506 } 2507 2508 static WidgetPainter fromJsVar(T)(T t) { 2509 return WidgetPainter.init; 2510 } 2511 // done.......... 2512 } 2513 2514 /++ 2515 History: 2516 Added Oct 28, 2020 2517 +/ 2518 struct ControlledBy_(T, Args...) { 2519 Args args; 2520 this(Args args) { 2521 this.args = args; 2522 } 2523 2524 private T construct(Widget parent) { 2525 return new T(args, parent); 2526 } 2527 } 2528 2529 /++ 2530 History: 2531 Added Oct 28, 2020 2532 +/ 2533 auto ControlledBy(T, Args...)(Args args) { 2534 return ControlledBy_!(T, Args)(args); 2535 } 2536 2537 struct ContainerMeta { 2538 string name; 2539 ContainerMeta[] children; 2540 Widget function(Widget parent) factory; 2541 2542 Widget instantiate(Widget parent) { 2543 auto n = factory(parent); 2544 n.name = name; 2545 foreach(child; children) 2546 child.instantiate(n); 2547 return n; 2548 } 2549 } 2550 2551 template Container(CArgs...) { 2552 static if(CArgs.length && is(CArgs[0] : Widget)) { 2553 private alias Super = CArgs[0]; 2554 private alias CArgs2 = CArgs[1 .. $]; 2555 } else { 2556 private alias Super = Layout; 2557 private alias CArgs2 = CArgs; 2558 } 2559 2560 class Container : Super { 2561 this(Widget parent) { super(parent); } 2562 2563 // just to partially support old gdc versions 2564 version(GNU) { 2565 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 2566 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 2567 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 2568 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 2569 } else mixin(q{ 2570 static foreach(Arg; CArgs2) { 2571 mixin Arg.MethodOverride!(Arg); 2572 } 2573 }); 2574 2575 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 2576 return ContainerMeta( 2577 name, 2578 children.dup, 2579 function (Widget parent) { return new typeof(this)(parent); } 2580 ); 2581 } 2582 2583 static ContainerMeta opCall(ContainerMeta[] children...) { 2584 return opCall(null, children); 2585 } 2586 } 2587 } 2588 2589 /++ 2590 The data controller widget is created by reflecting over the given 2591 data type. You can use [ControlledBy] as a UDA on a struct or 2592 just let it create things automatically. 2593 2594 Unlike [dialog], this uses real-time updating of the data and 2595 you add it to another window yourself. 2596 2597 --- 2598 struct Test { 2599 int x; 2600 int y; 2601 } 2602 2603 auto window = new Window(); 2604 auto dcw = new DataControllerWidget!Test(new Test, window); 2605 --- 2606 2607 The way it works is any public members are given a widget based 2608 on their data type, and public methods trigger an action button 2609 if no relevant parameters or a dialog action if it does have 2610 parameters, similar to the [menu] facility. 2611 2612 If you change data programmatically, without going through the 2613 DataControllerWidget methods, you will have to tell it something 2614 has changed and it needs to redraw. This is done with the `invalidate` 2615 method. 2616 2617 History: 2618 Added Oct 28, 2020 2619 +/ 2620 /// Group: generating_from_code 2621 class DataControllerWidget(T) : Widget { 2622 static if(is(T == class) || is(T : const E[], E)) 2623 private alias Tref = T; 2624 else 2625 private alias Tref = T*; 2626 2627 Tref datum; 2628 2629 /++ 2630 See_also: [addDataControllerWidget] 2631 +/ 2632 this(Tref datum, Widget parent) { 2633 this.datum = datum; 2634 2635 Widget cp = this; 2636 2637 super(parent); 2638 2639 foreach(attr; __traits(getAttributes, T)) 2640 static if(is(typeof(attr) == ContainerMeta)) { 2641 cp = attr.instantiate(this); 2642 } 2643 2644 auto def = this.getByName("default"); 2645 if(def !is null) 2646 cp = def; 2647 2648 Widget helper(string name) { 2649 auto maybe = this.getByName(name); 2650 if(maybe is null) 2651 return cp; 2652 return maybe; 2653 2654 } 2655 2656 foreach(member; __traits(allMembers, T)) 2657 static if(member != "this") // wtf 2658 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 2659 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member)); 2660 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) 2661 w.addEventListener("triggered", &__traits(getMember, this.datum, member)); 2662 else static if(is(w : DropDownSelection)) 2663 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 2664 else 2665 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 2666 } 2667 } 2668 2669 Widget[string] memberWidgets; 2670 } 2671 2672 void genericSetValue(T, W)(T* where, W what) { 2673 import std.conv; 2674 *where = to!T(what); 2675 //*where = cast(T) stringToLong(what); 2676 } 2677 2678 // FIXME: integrate with AutomaticDialog 2679 static auto widgetFor(alias tt, P)(P valptr, Widget parent) { 2680 static if(controlledByCount!tt == 1) { 2681 foreach(i, attr; __traits(getAttributes, tt)) { 2682 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 2683 auto w = attr.construct(parent); 2684 static if(__traits(compiles, w.setPosition(*valptr))) 2685 w.setPosition(*valptr); 2686 else static if(__traits(compiles, w.setValue(*valptr))) 2687 w.setValue(*valptr); 2688 return w; 2689 } 2690 } 2691 } else static if(controlledByCount!tt == 0) { 2692 static if(is(typeof(tt) == enum)) { 2693 auto dds = new DropDownSelection(parent); 2694 foreach(idx, option; __traits(allMembers, typeof(tt))) { 2695 dds.addOption(option); 2696 if(__traits(getMember, typeof(tt), option) == *valptr) 2697 dds.setSelection(cast(int) idx); 2698 } 2699 return dds; 2700 } else static if(is(typeof(tt) : const long)) { 2701 static assert(0); 2702 } else static if(is(typeof(tt) : const string)) { 2703 static assert(0); 2704 } 2705 } else static assert(0, "multiple controllers not yet supported"); 2706 } 2707 2708 private template controlledByCount(alias tt) { 2709 static int helper() { 2710 int count; 2711 foreach(i, attr; __traits(getAttributes, tt)) 2712 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 2713 count++; 2714 return count; 2715 } 2716 2717 enum controlledByCount = helper; 2718 } 2719 2720 /++ 2721 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 2722 2723 +/ 2724 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t) if(is(T == class)) { 2725 return new DataControllerWidget!T(t, parent); 2726 } 2727 2728 /// ditto 2729 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t) if(is(T == struct)) { 2730 return new DataControllerWidget!T(t, parent); 2731 } 2732 2733 /+ 2734 styleClass = ""; 2735 2736 widget.computedStyle 2737 +/ 2738 struct StyleInformation { 2739 private Widget w; 2740 private BaseVisualTheme visualTheme; 2741 2742 private this(Widget w) { 2743 this.w = w; 2744 this.visualTheme = WidgetPainter.visualTheme; 2745 } 2746 2747 // FIXME: clear this upon a X server disconnect 2748 private static OperatingSystemFont[string] fontCache; 2749 2750 T getProperty(T)(string name, lazy T default_) { 2751 if(visualTheme !is null) { 2752 auto str = visualTheme.getPropertyString(w, name); 2753 if(str is null) 2754 return default_; 2755 static if(is(T == Color)) 2756 return Color.fromString(str); 2757 else static if(is(T == Measurement)) 2758 return Measurement(cast(int) toInternal!int(str)); 2759 else static if(is(T == OperatingSystemFont)) { 2760 if(auto f = str in fontCache) 2761 return *f; 2762 else 2763 return fontCache[str] = new OperatingSystemFont(str); 2764 } else static if(is(T == FrameStyle)) { 2765 switch(str) { 2766 default: 2767 return FrameStyle.none; 2768 foreach(style; __traits(allMembers, FrameStyle)) 2769 case style: 2770 return __traits(getMember, FrameStyle, style); 2771 } 2772 } else static assert(0); 2773 } else 2774 return default_; 2775 } 2776 2777 static struct Measurement { 2778 int value; 2779 alias value this; 2780 } 2781 2782 private static auto extractStyleProperty(string name)(Widget w) { 2783 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 2784 w.useStyleProperties((props) { 2785 prop = __traits(getMember, props, name); 2786 }); 2787 return prop; 2788 } 2789 2790 @property: 2791 2792 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 2793 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 2794 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 2795 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 2796 2797 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 2798 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 2799 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 2800 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 2801 2802 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 2803 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 2804 2805 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 2806 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 2807 2808 2809 Color backgroundColor() { return getProperty("background-color", extractStyleProperty!"backgroundColor"(w)); } 2810 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 2811 2812 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 2813 2814 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 2815 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 2816 2817 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 2818 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 2819 2820 2821 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 2822 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 2823 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 2824 2825 Color activeTabColor() { return lightAccentColor; } 2826 Color buttonColor() { return windowBackgroundColor; } 2827 Color depressedButtonColor() { return darkAccentColor; } 2828 Color hoveringColor() { return Color(228, 228, 228); } 2829 Color activeListXorColor() { 2830 auto c = WidgetPainter.visualTheme.selectionColor(); 2831 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 2832 } 2833 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 2834 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 2835 } 2836 2837 2838 2839 // pragma(msg, __traits(classInstanceSize, Widget)); 2840 2841 /*private*/ template EventString(E) { 2842 static if(is(typeof(E.EventString))) 2843 enum EventString = E.EventString; 2844 else 2845 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 2846 } 2847 2848 /*private*/ template EventStringIdentifier(E) { 2849 string helper() { 2850 auto es = EventString!E; 2851 char[] id = new char[](es.length * 2); 2852 size_t idx; 2853 foreach(char ch; es) { 2854 id[idx++] = cast(char)('a' + (ch >> 4)); 2855 id[idx++] = cast(char)('a' + (ch & 0x0f)); 2856 } 2857 return cast(string) id; 2858 } 2859 2860 enum EventStringIdentifier = helper(); 2861 } 2862 2863 2864 template classStaticallyEmits(This, EventType) { 2865 static if(is(This Base == super)) 2866 static if(is(Base : Widget)) 2867 enum baseEmits = classStaticallyEmits!(Base, EventType); 2868 else 2869 enum baseEmits = false; 2870 else 2871 enum baseEmits = false; 2872 2873 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 2874 2875 enum classStaticallyEmits = thisEmits || baseEmits; 2876 } 2877 2878 /++ 2879 Nests an opengl capable window inside this window as a widget. 2880 2881 You may also just want to create an additional [SimpleWindow] with 2882 [OpenGlOptions.yes] yourself. 2883 2884 An OpenGL widget cannot have child widgets. It will throw if you try. 2885 +/ 2886 static if(OpenGlEnabled) 2887 class OpenGlWidget : Widget { 2888 SimpleWindow win; 2889 2890 /// 2891 this(Widget parent) { 2892 this.parentWindow = parent.parentWindow; 2893 2894 SimpleWindow pwin = this.parentWindow.win; 2895 2896 2897 version(win32_widgets) { 2898 HWND phwnd; 2899 auto wtf = parent; 2900 while(wtf) { 2901 if(wtf.hwnd) { 2902 phwnd = wtf.hwnd; 2903 break; 2904 } 2905 wtf = wtf.parent; 2906 } 2907 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 2908 if(phwnd) 2909 pwin = new SimpleWindow(phwnd); 2910 } 2911 2912 win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin); 2913 super(parent); 2914 2915 windowsetup(win); 2916 } 2917 2918 protected void windowsetup(SimpleWindow w) { 2919 /* 2920 win.onFocusChange = (bool getting) { 2921 if(getting) 2922 this.focus(); 2923 }; 2924 */ 2925 2926 version(win32_widgets) { 2927 Widget.nativeMapping[win.hwnd] = this; 2928 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 2929 } else { 2930 win.setEventHandlers( 2931 (MouseEvent e) { 2932 Widget p = this; 2933 while(p ! is parentWindow) { 2934 e.x += p.x; 2935 e.y += p.y; 2936 p = p.parent; 2937 } 2938 parentWindow.dispatchMouseEvent(e); 2939 }, 2940 (KeyEvent e) { 2941 //import std.stdio; 2942 //writefln("%x %s", cast(uint) e.key, e.key); 2943 parentWindow.dispatchKeyEvent(e); 2944 }, 2945 (dchar e) { 2946 parentWindow.dispatchCharEvent(e); 2947 }, 2948 ); 2949 } 2950 2951 } 2952 2953 override void paint(WidgetPainter painter) { 2954 win.redrawOpenGlSceneNow(); 2955 } 2956 2957 void redrawOpenGlScene(void delegate() dg) { 2958 win.redrawOpenGlScene = dg; 2959 } 2960 2961 override void showing(bool s, bool recalc) { 2962 auto cur = hidden; 2963 win.hidden = !s; 2964 if(cur != s && s) 2965 redraw(); 2966 } 2967 2968 /// OpenGL widgets cannot have child widgets. Do not call this. 2969 /* @disable */ final override void addChild(Widget, int) { 2970 throw new Error("cannot add children to OpenGL widgets"); 2971 } 2972 2973 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 2974 /// Keep in mind that events like mouse coordinates are still relative to your size. 2975 override void registerMovement() { 2976 //import std.stdio; writefln("%d %d %d %d", x,y,width,height); 2977 version(win32_widgets) 2978 auto pos = getChildPositionRelativeToParentHwnd(this); 2979 else 2980 auto pos = getChildPositionRelativeToParentOrigin(this); 2981 win.moveResize(pos[0], pos[1], width, height); 2982 2983 win.setAsCurrentOpenGlContext(); 2984 sendResizeEvent(); 2985 } 2986 2987 //void delegate() drawFrame; 2988 } 2989 2990 version(custom_widgets) 2991 private alias ListWidgetBase = ScrollableWidget; 2992 else 2993 private alias ListWidgetBase = Widget; 2994 2995 /++ 2996 2997 +/ 2998 class ListWidget : ListWidgetBase { 2999 /// 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. 3000 mixin Emits!(ChangeEvent!void); 3001 3002 static struct Option { 3003 string label; 3004 bool selected; 3005 } 3006 3007 void setSelection(int y) { 3008 if(!multiSelect) 3009 foreach(ref opt; options) 3010 opt.selected = false; 3011 if(y >= 0 && y < options.length) 3012 options[y].selected = !options[y].selected; 3013 3014 this.emit!(ChangeEvent!void)(delegate {}); 3015 3016 version(custom_widgets) 3017 redraw(); 3018 } 3019 3020 version(custom_widgets) 3021 override void defaultEventHandler_click(ClickEvent event) { 3022 this.focus(); 3023 auto y = (event.clientY - 4) / Window.lineHeight; 3024 if(y >= 0 && y < options.length) { 3025 setSelection(y); 3026 } 3027 super.defaultEventHandler_click(event); 3028 } 3029 3030 this(Widget parent = null) { 3031 tabStop = false; 3032 super(parent); 3033 version(win32_widgets) 3034 createWin32Window(this, WC_LISTBOX, "", 3035 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 3036 } 3037 3038 version(win32_widgets) 3039 override void handleWmCommand(ushort code, ushort id) { 3040 switch(code) { 3041 case LBN_SELCHANGE: 3042 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 3043 setSelection(cast(int) sel); 3044 break; 3045 default: 3046 } 3047 } 3048 3049 3050 version(custom_widgets) 3051 override void paintFrameAndBackground(WidgetPainter painter) { 3052 draw3dFrame(this, painter, FrameStyle.sunk, Color.white); 3053 } 3054 3055 version(custom_widgets) 3056 override void paint(WidgetPainter painter) { 3057 auto cs = getComputedStyle(); 3058 auto pos = Point(4, 4); 3059 foreach(idx, option; options) { 3060 painter.fillColor = Color.white; 3061 painter.outlineColor = Color.white; 3062 painter.drawRectangle(pos, width - 8, Window.lineHeight); 3063 painter.outlineColor = cs.foregroundColor; 3064 painter.drawText(pos, option.label); 3065 if(option.selected) { 3066 painter.rasterOp = RasterOp.xor; 3067 painter.outlineColor = Color.white; 3068 painter.fillColor = cs.activeListXorColor; 3069 painter.drawRectangle(pos, width - 8, Window.lineHeight); 3070 painter.rasterOp = RasterOp.normal; 3071 } 3072 pos.y += Window.lineHeight; 3073 } 3074 } 3075 3076 3077 void addOption(string text) { 3078 options ~= Option(text); 3079 version(win32_widgets) { 3080 WCharzBuffer buffer = WCharzBuffer(text); 3081 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 3082 } 3083 version(custom_widgets) { 3084 setContentSize(width, cast(int) (options.length * Window.lineHeight)); 3085 redraw(); 3086 } 3087 } 3088 3089 void clear() { 3090 options = null; 3091 version(win32_widgets) { 3092 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 3093 {} 3094 3095 } else version(custom_widgets) { 3096 redraw(); 3097 } 3098 } 3099 3100 Option[] options; 3101 version(win32_widgets) 3102 enum multiSelect = false; /// not implemented yet 3103 else 3104 bool multiSelect; 3105 3106 override int heightStretchiness() { return 6; } 3107 } 3108 3109 3110 3111 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 3112 enum ScrollBarShowPolicy { 3113 automatic, /// automatically show the scroll bar if it is necessary 3114 never, /// never show the scroll bar (scrolling must be done programmatically) 3115 always /// always show the scroll bar, even if it is disabled 3116 } 3117 3118 /++ 3119 FIXME ScrollBarShowPolicy 3120 FIXME: use the ScrollMessageWidget in here now that it exists 3121 +/ 3122 class ScrollableWidget : Widget { 3123 // FIXME: make line size configurable 3124 // FIXME: add keyboard controls 3125 version(win32_widgets) { 3126 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 3127 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 3128 auto pos = HIWORD(wParam); 3129 auto m = LOWORD(wParam); 3130 3131 // FIXME: I can reintroduce the 3132 // scroll bars now by using this 3133 // in the top-level window handler 3134 // to forward comamnds 3135 auto scrollbarHwnd = lParam; 3136 switch(m) { 3137 case SB_BOTTOM: 3138 if(msg == WM_HSCROLL) 3139 horizontalScrollTo(contentWidth_); 3140 else 3141 verticalScrollTo(contentHeight_); 3142 break; 3143 case SB_TOP: 3144 if(msg == WM_HSCROLL) 3145 horizontalScrollTo(0); 3146 else 3147 verticalScrollTo(0); 3148 break; 3149 case SB_ENDSCROLL: 3150 // idk 3151 break; 3152 case SB_LINEDOWN: 3153 if(msg == WM_HSCROLL) 3154 horizontalScroll(16); 3155 else 3156 verticalScroll(16); 3157 break; 3158 case SB_LINEUP: 3159 if(msg == WM_HSCROLL) 3160 horizontalScroll(-16); 3161 else 3162 verticalScroll(-16); 3163 break; 3164 case SB_PAGEDOWN: 3165 if(msg == WM_HSCROLL) 3166 horizontalScroll(100); 3167 else 3168 verticalScroll(100); 3169 break; 3170 case SB_PAGEUP: 3171 if(msg == WM_HSCROLL) 3172 horizontalScroll(-100); 3173 else 3174 verticalScroll(-100); 3175 break; 3176 case SB_THUMBPOSITION: 3177 case SB_THUMBTRACK: 3178 if(msg == WM_HSCROLL) 3179 horizontalScrollTo(pos); 3180 else 3181 verticalScrollTo(pos); 3182 3183 if(m == SB_THUMBTRACK) { 3184 // the event loop doesn't seem to carry on with a requested redraw.. 3185 // so we request it to get our dirty bit set... 3186 redraw(); 3187 // then we need to immediately actually redraw it too for instant feedback to user 3188 actualRedraw(); 3189 } 3190 break; 3191 default: 3192 } 3193 } 3194 return 0; 3195 } 3196 } 3197 /// 3198 this(Widget parent) { 3199 this.parentWindow = parent.parentWindow; 3200 3201 version(win32_widgets) { 3202 static bool classRegistered = false; 3203 if(!classRegistered) { 3204 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 3205 WNDCLASSEX wc; 3206 wc.cbSize = wc.sizeof; 3207 wc.hInstance = hInstance; 3208 wc.lpfnWndProc = &DefWindowProc; 3209 wc.lpszClassName = "arsd_minigui_ScrollableWidget"w.ptr; 3210 if(!RegisterClassExW(&wc)) 3211 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 3212 classRegistered = true; 3213 } 3214 3215 createWin32Window(this, "arsd_minigui_ScrollableWidget"w, "", 3216 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 3217 super(parent); 3218 } else version(custom_widgets) { 3219 outerContainer = new ScrollableContainerWidget(this, parent); 3220 super(outerContainer); 3221 } else static assert(0); 3222 } 3223 3224 version(custom_widgets) 3225 ScrollableContainerWidget outerContainer; 3226 3227 override void defaultEventHandler_click(ClickEvent event) { 3228 if(event.button == MouseButton.wheelUp) 3229 verticalScroll(-16); 3230 if(event.button == MouseButton.wheelDown) 3231 verticalScroll(16); 3232 super.defaultEventHandler_click(event); 3233 } 3234 3235 override void defaultEventHandler_keydown(KeyDownEvent event) { 3236 switch(event.key) { 3237 case Key.Left: 3238 horizontalScroll(-16); 3239 break; 3240 case Key.Right: 3241 horizontalScroll(16); 3242 break; 3243 case Key.Up: 3244 verticalScroll(-16); 3245 break; 3246 case Key.Down: 3247 verticalScroll(16); 3248 break; 3249 case Key.Home: 3250 verticalScrollTo(0); 3251 break; 3252 case Key.End: 3253 verticalScrollTo(contentHeight); 3254 break; 3255 case Key.PageUp: 3256 verticalScroll(-160); 3257 break; 3258 case Key.PageDown: 3259 verticalScroll(160); 3260 break; 3261 default: 3262 } 3263 super.defaultEventHandler_keydown(event); 3264 } 3265 3266 3267 version(win32_widgets) 3268 override void recomputeChildLayout() { 3269 super.recomputeChildLayout(); 3270 SCROLLINFO info; 3271 info.cbSize = info.sizeof; 3272 info.nPage = viewportHeight; 3273 info.fMask = SIF_PAGE | SIF_RANGE; 3274 info.nMin = 0; 3275 info.nMax = contentHeight_; 3276 SetScrollInfo(hwnd, SB_VERT, &info, true); 3277 3278 info.cbSize = info.sizeof; 3279 info.nPage = viewportWidth; 3280 info.fMask = SIF_PAGE | SIF_RANGE; 3281 info.nMin = 0; 3282 info.nMax = contentWidth_; 3283 SetScrollInfo(hwnd, SB_HORZ, &info, true); 3284 } 3285 3286 3287 3288 /* 3289 Scrolling 3290 ------------ 3291 3292 You are assigned a width and a height by the layout engine, which 3293 is your viewport box. However, you may draw more than that by setting 3294 a contentWidth and contentHeight. 3295 3296 If these can be contained by the viewport, no scrollbar is displayed. 3297 If they cannot fit though, it will automatically show scroll as necessary. 3298 3299 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 3300 is zero, no vertical scrolling is performed. 3301 3302 If scrolling is necessary, the lib will automatically work with the bars. 3303 When you redraw, the origin and clipping info in the painter is set so if 3304 you just draw everything, it will work, but you can be more efficient by checking 3305 the viewportWidth, viewportHeight, and scrollOrigin members. 3306 */ 3307 3308 /// 3309 final @property int viewportWidth() { 3310 return width - (showingVerticalScroll ? 16 : 0); 3311 } 3312 /// 3313 final @property int viewportHeight() { 3314 return height - (showingHorizontalScroll ? 16 : 0); 3315 } 3316 3317 // FIXME property 3318 Point scrollOrigin_; 3319 3320 /// 3321 final const(Point) scrollOrigin() { 3322 return scrollOrigin_; 3323 } 3324 3325 // the user sets these two 3326 private int contentWidth_ = 0; 3327 private int contentHeight_ = 0; 3328 3329 /// 3330 int contentWidth() { return contentWidth_; } 3331 /// 3332 int contentHeight() { return contentHeight_; } 3333 3334 /// 3335 void setContentSize(int width, int height) { 3336 contentWidth_ = width; 3337 contentHeight_ = height; 3338 3339 version(custom_widgets) { 3340 if(showingVerticalScroll || showingHorizontalScroll) { 3341 outerContainer.recomputeChildLayout(); 3342 } 3343 3344 if(showingVerticalScroll()) 3345 outerContainer.verticalScrollBar.redraw(); 3346 if(showingHorizontalScroll()) 3347 outerContainer.horizontalScrollBar.redraw(); 3348 } else version(win32_widgets) { 3349 recomputeChildLayout(); 3350 } else static assert(0); 3351 } 3352 3353 /// 3354 void verticalScroll(int delta) { 3355 verticalScrollTo(scrollOrigin.y + delta); 3356 } 3357 /// 3358 void verticalScrollTo(int pos) { 3359 scrollOrigin_.y = pos; 3360 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 3361 scrollOrigin_.y = contentHeight - viewportHeight; 3362 3363 if(scrollOrigin_.y < 0) 3364 scrollOrigin_.y = 0; 3365 3366 version(win32_widgets) { 3367 SCROLLINFO info; 3368 info.cbSize = info.sizeof; 3369 info.fMask = SIF_POS; 3370 info.nPos = scrollOrigin_.y; 3371 SetScrollInfo(hwnd, SB_VERT, &info, true); 3372 } else version(custom_widgets) { 3373 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 3374 } else static assert(0); 3375 3376 redraw(); 3377 } 3378 3379 /// 3380 void horizontalScroll(int delta) { 3381 horizontalScrollTo(scrollOrigin.x + delta); 3382 } 3383 /// 3384 void horizontalScrollTo(int pos) { 3385 scrollOrigin_.x = pos; 3386 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 3387 scrollOrigin_.x = contentWidth - viewportWidth; 3388 3389 if(scrollOrigin_.x < 0) 3390 scrollOrigin_.x = 0; 3391 3392 version(win32_widgets) { 3393 SCROLLINFO info; 3394 info.cbSize = info.sizeof; 3395 info.fMask = SIF_POS; 3396 info.nPos = scrollOrigin_.x; 3397 SetScrollInfo(hwnd, SB_HORZ, &info, true); 3398 } else version(custom_widgets) { 3399 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 3400 } else static assert(0); 3401 3402 redraw(); 3403 } 3404 /// 3405 void scrollTo(Point p) { 3406 verticalScrollTo(p.y); 3407 horizontalScrollTo(p.x); 3408 } 3409 3410 /// 3411 void ensureVisibleInScroll(Point p) { 3412 auto rect = viewportRectangle(); 3413 if(rect.contains(p)) 3414 return; 3415 if(p.x < rect.left) 3416 horizontalScroll(p.x - rect.left); 3417 else if(p.x > rect.right) 3418 horizontalScroll(p.x - rect.right); 3419 3420 if(p.y < rect.top) 3421 verticalScroll(p.y - rect.top); 3422 else if(p.y > rect.bottom) 3423 verticalScroll(p.y - rect.bottom); 3424 } 3425 3426 /// 3427 void ensureVisibleInScroll(Rectangle rect) { 3428 ensureVisibleInScroll(rect.upperLeft); 3429 ensureVisibleInScroll(rect.lowerRight); 3430 } 3431 3432 /// 3433 Rectangle viewportRectangle() { 3434 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 3435 } 3436 3437 /// 3438 bool showingHorizontalScroll() { 3439 return contentWidth > width; 3440 } 3441 /// 3442 bool showingVerticalScroll() { 3443 return contentHeight > height; 3444 } 3445 3446 /// This is called before the ordinary paint delegate, 3447 /// giving you a chance to draw the window frame, etc, 3448 /// before the scroll clip takes effect 3449 void paintFrameAndBackground(WidgetPainter painter) { 3450 version(win32_widgets) { 3451 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 3452 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 3453 // since the pen is null, to fill the whole space, we need the +1 on both. 3454 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 3455 SelectObject(painter.impl.hdc, p); 3456 SelectObject(painter.impl.hdc, b); 3457 } 3458 3459 } 3460 3461 // make space for the scroll bar, and that's it. 3462 final override int paddingRight() { return 16; } 3463 final override int paddingBottom() { return 16; } 3464 3465 /* 3466 END SCROLLING 3467 */ 3468 3469 override WidgetPainter draw() { 3470 int x = this.x, y = this.y; 3471 auto parent = this.parent; 3472 while(parent) { 3473 x += parent.x; 3474 y += parent.y; 3475 parent = parent.parent; 3476 } 3477 3478 //version(win32_widgets) { 3479 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw() : parentWindow.win.draw(); 3480 //} else { 3481 auto painter = parentWindow.win.draw(); 3482 //} 3483 painter.originX = x; 3484 painter.originY = y; 3485 3486 painter.originX = painter.originX - scrollOrigin.x; 3487 painter.originY = painter.originY - scrollOrigin.y; 3488 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 3489 3490 return WidgetPainter(painter, this); 3491 } 3492 3493 override protected void privatePaint(WidgetPainter painter, int lox, int loy, bool force = false) { 3494 if(hidden) 3495 return; 3496 3497 //version(win32_widgets) 3498 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw() : parentWindow.win.draw(); 3499 3500 painter.originX = lox + x; 3501 painter.originY = loy + y; 3502 3503 bool actuallyPainted = false; 3504 3505 if(force || redrawRequested) { 3506 painter.setClipRectangle(Point(0, 0), width, height); 3507 paintFrameAndBackground(painter); 3508 } 3509 3510 painter.originX = painter.originX - scrollOrigin.x; 3511 painter.originY = painter.originY - scrollOrigin.y; 3512 if(force || redrawRequested) { 3513 painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 3514 3515 //erase(painter); // we paintFrameAndBackground above so no need 3516 if(painter.visualTheme) 3517 painter.visualTheme.doPaint(this, painter); 3518 else 3519 paint(painter); 3520 3521 actuallyPainted = true; 3522 redrawRequested = false; 3523 } 3524 foreach(child; children) { 3525 if(cast(FixedPosition) child) 3526 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, actuallyPainted); 3527 else 3528 child.privatePaint(painter, painter.originX, painter.originY, actuallyPainted); 3529 } 3530 } 3531 } 3532 3533 version(custom_widgets) 3534 private class ScrollableContainerWidget : Widget { 3535 3536 ScrollableWidget sw; 3537 3538 VerticalScrollbar verticalScrollBar; 3539 HorizontalScrollbar horizontalScrollBar; 3540 3541 this(ScrollableWidget sw, Widget parent) { 3542 this.sw = sw; 3543 3544 this.tabStop = false; 3545 3546 horizontalScrollBar = new HorizontalScrollbar(this); 3547 verticalScrollBar = new VerticalScrollbar(this); 3548 3549 horizontalScrollBar.showing_ = false; 3550 verticalScrollBar.showing_ = false; 3551 3552 horizontalScrollBar.addEventListener("scrolltonextline", { 3553 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 3554 sw.horizontalScrollTo(horizontalScrollBar.position); 3555 }); 3556 horizontalScrollBar.addEventListener("scrolltopreviousline", { 3557 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 3558 sw.horizontalScrollTo(horizontalScrollBar.position); 3559 }); 3560 verticalScrollBar.addEventListener("scrolltonextline", { 3561 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 3562 sw.verticalScrollTo(verticalScrollBar.position); 3563 }); 3564 verticalScrollBar.addEventListener("scrolltopreviousline", { 3565 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 3566 sw.verticalScrollTo(verticalScrollBar.position); 3567 }); 3568 horizontalScrollBar.addEventListener("scrolltonextpage", { 3569 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 3570 sw.horizontalScrollTo(horizontalScrollBar.position); 3571 }); 3572 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 3573 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 3574 sw.horizontalScrollTo(horizontalScrollBar.position); 3575 }); 3576 verticalScrollBar.addEventListener("scrolltonextpage", { 3577 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 3578 sw.verticalScrollTo(verticalScrollBar.position); 3579 }); 3580 verticalScrollBar.addEventListener("scrolltopreviouspage", { 3581 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 3582 sw.verticalScrollTo(verticalScrollBar.position); 3583 }); 3584 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 3585 horizontalScrollBar.setPosition(event.intValue); 3586 sw.horizontalScrollTo(horizontalScrollBar.position); 3587 }); 3588 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 3589 verticalScrollBar.setPosition(event.intValue); 3590 sw.verticalScrollTo(verticalScrollBar.position); 3591 }); 3592 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 3593 horizontalScrollBar.setPosition(event.intValue); 3594 sw.horizontalScrollTo(horizontalScrollBar.position); 3595 }); 3596 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 3597 verticalScrollBar.setPosition(event.intValue); 3598 }); 3599 3600 super(parent); 3601 } 3602 3603 // this is supposed to be basically invisible... 3604 override int minWidth() { return sw.minWidth; } 3605 override int minHeight() { return sw.minHeight; } 3606 override int maxWidth() { return sw.maxWidth; } 3607 override int maxHeight() { return sw.maxHeight; } 3608 override int widthStretchiness() { return sw.widthStretchiness; } 3609 override int heightStretchiness() { return sw.heightStretchiness; } 3610 override int marginLeft() { return sw.marginLeft; } 3611 override int marginRight() { return sw.marginRight; } 3612 override int marginTop() { return sw.marginTop; } 3613 override int marginBottom() { return sw.marginBottom; } 3614 override int paddingLeft() { return sw.paddingLeft; } 3615 override int paddingRight() { return sw.paddingRight; } 3616 override int paddingTop() { return sw.paddingTop; } 3617 override int paddingBottom() { return sw.paddingBottom; } 3618 override void focus() { sw.focus(); } 3619 3620 3621 override void recomputeChildLayout() { 3622 if(sw is null) return; 3623 3624 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 3625 if(horizontalScrollBar && verticalScrollBar) { 3626 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 3627 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 3628 horizontalScrollBar.x = 0; 3629 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 3630 3631 verticalScrollBar.width = verticalScrollBar.minWidth(); 3632 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 3633 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 3634 verticalScrollBar.y = 0 + 2; 3635 3636 sw.x = 0; 3637 sw.y = 0; 3638 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 3639 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 3640 3641 if(sw.contentWidth_ <= this.width) 3642 sw.scrollOrigin_.x = 0; 3643 if(sw.contentHeight_ <= this.height) 3644 sw.scrollOrigin_.y = 0; 3645 3646 horizontalScrollBar.recomputeChildLayout(); 3647 verticalScrollBar.recomputeChildLayout(); 3648 sw.recomputeChildLayout(); 3649 } 3650 3651 if(sw.contentWidth_ <= this.width) 3652 sw.scrollOrigin_.x = 0; 3653 if(sw.contentHeight_ <= this.height) 3654 sw.scrollOrigin_.y = 0; 3655 3656 if(sw.showingHorizontalScroll()) 3657 horizontalScrollBar.showing = true; 3658 else 3659 horizontalScrollBar.showing = false; 3660 if(sw.showingVerticalScroll()) 3661 verticalScrollBar.showing = true; 3662 else 3663 verticalScrollBar.showing = false; 3664 3665 3666 verticalScrollBar.setViewableArea(sw.viewportHeight()); 3667 verticalScrollBar.setMax(sw.contentHeight); 3668 verticalScrollBar.setPosition(sw.scrollOrigin.y); 3669 3670 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 3671 horizontalScrollBar.setMax(sw.contentWidth); 3672 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 3673 } 3674 } 3675 3676 /* 3677 class ScrollableClientWidget : Widget { 3678 this(Widget parent) { 3679 super(parent); 3680 } 3681 override void paint(WidgetPainter p) { 3682 parent.paint(p); 3683 } 3684 } 3685 */ 3686 3687 /++ 3688 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. 3689 +/ 3690 abstract class Slider : Widget { 3691 this(int min, int max, int step, Widget parent) { 3692 min_ = min; 3693 max_ = max; 3694 step_ = step; 3695 page_ = step; 3696 super(parent); 3697 } 3698 3699 private int min_; 3700 private int max_; 3701 private int step_; 3702 private int position_; 3703 private int page_; 3704 3705 // selection start and selection end 3706 // tics 3707 // tooltip? 3708 // some way to see and just type the value 3709 // win32 buddy controls are labels 3710 3711 /// 3712 void setMin(int a) { 3713 min_ = a; 3714 version(custom_widgets) 3715 redraw(); 3716 version(win32_widgets) 3717 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 3718 } 3719 /// 3720 int min() { 3721 return min_; 3722 } 3723 /// 3724 void setMax(int a) { 3725 max_ = a; 3726 version(custom_widgets) 3727 redraw(); 3728 version(win32_widgets) 3729 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 3730 } 3731 /// 3732 int max() { 3733 return max_; 3734 } 3735 /// 3736 void setPosition(int a) { 3737 if(a > max) 3738 a = max; 3739 if(a < min) 3740 a = min; 3741 position_ = a; 3742 version(custom_widgets) 3743 setPositionCustom(a); 3744 3745 version(win32_widgets) 3746 setPositionWindows(a); 3747 } 3748 version(win32_widgets) { 3749 protected abstract void setPositionWindows(int a); 3750 } 3751 3752 protected abstract int win32direction(); 3753 3754 /// 3755 int position() { 3756 return position_; 3757 } 3758 /// 3759 void setStep(int a) { 3760 step_ = a; 3761 version(win32_widgets) 3762 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 3763 } 3764 /// 3765 int step() { 3766 return step_; 3767 } 3768 /// 3769 void setPageSize(int a) { 3770 page_ = a; 3771 version(win32_widgets) 3772 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 3773 } 3774 /// 3775 int pageSize() { 3776 return page_; 3777 } 3778 3779 private void notify() { 3780 auto event = new ChangeEvent!int(this, &this.position); 3781 event.dispatch(); 3782 } 3783 3784 version(win32_widgets) 3785 void win32Setup(int style) { 3786 createWin32Window(this, TRACKBAR_CLASS, "", 3787 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 3788 3789 // the trackbar sends the same messages as scroll, which 3790 // our other layer sends as these... just gonna translate 3791 // here 3792 this.addDirectEventListener("scrolltoposition", (Event event) { 3793 event.stopPropagation(); 3794 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 3795 notify(); 3796 }); 3797 this.addDirectEventListener("scrolltonextline", (Event event) { 3798 event.stopPropagation(); 3799 this.setPosition(this.position + this.step_ * this.win32direction); 3800 notify(); 3801 }); 3802 this.addDirectEventListener("scrolltopreviousline", (Event event) { 3803 event.stopPropagation(); 3804 this.setPosition(this.position - this.step_ * this.win32direction); 3805 notify(); 3806 }); 3807 this.addDirectEventListener("scrolltonextpage", (Event event) { 3808 event.stopPropagation(); 3809 this.setPosition(this.position + this.page_ * this.win32direction); 3810 notify(); 3811 }); 3812 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 3813 event.stopPropagation(); 3814 this.setPosition(this.position - this.page_ * this.win32direction); 3815 notify(); 3816 }); 3817 3818 setMin(min_); 3819 setMax(max_); 3820 setStep(step_); 3821 setPageSize(page_); 3822 } 3823 3824 version(custom_widgets) { 3825 protected MouseTrackingWidget thumb; 3826 3827 protected abstract void setPositionCustom(int a); 3828 3829 override void defaultEventHandler_keydown(KeyDownEvent event) { 3830 switch(event.key) { 3831 case Key.Up: 3832 case Key.Right: 3833 setPosition(position() - step() * win32direction); 3834 changed(); 3835 break; 3836 case Key.Down: 3837 case Key.Left: 3838 setPosition(position() + step() * win32direction); 3839 changed(); 3840 break; 3841 case Key.Home: 3842 setPosition(win32direction > 0 ? min() : max()); 3843 changed(); 3844 break; 3845 case Key.End: 3846 setPosition(win32direction > 0 ? max() : min()); 3847 changed(); 3848 break; 3849 case Key.PageUp: 3850 setPosition(position() - pageSize() * win32direction); 3851 changed(); 3852 break; 3853 case Key.PageDown: 3854 setPosition(position() + pageSize() * win32direction); 3855 changed(); 3856 break; 3857 default: 3858 } 3859 super.defaultEventHandler_keydown(event); 3860 } 3861 3862 protected void changed() { 3863 auto ev = new ChangeEvent!int(this, &position); 3864 ev.dispatch(); 3865 } 3866 } 3867 } 3868 3869 /++ 3870 3871 +/ 3872 class VerticalSlider : Slider { 3873 this(int min, int max, int step, Widget parent) { 3874 version(custom_widgets) 3875 initialize(); 3876 3877 super(min, max, step, parent); 3878 3879 version(win32_widgets) 3880 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 3881 } 3882 3883 protected override int win32direction() { 3884 return -1; 3885 } 3886 3887 version(win32_widgets) 3888 protected override void setPositionWindows(int a) { 3889 // the windows thing makes the top 0 and i don't like that. 3890 SendMessage(hwnd, TBM_SETPOS, true, max - a); 3891 } 3892 3893 version(custom_widgets) 3894 private void initialize() { 3895 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 3896 3897 thumb.tabStop = false; 3898 3899 thumb.thumbWidth = width; 3900 thumb.thumbHeight = 16; 3901 3902 thumb.addEventListener(EventType.change, () { 3903 auto sx = thumb.positionY * max() / (thumb.height - 16); 3904 sx = max - sx; 3905 //informProgramThatUserChangedPosition(sx); 3906 3907 position_ = sx; 3908 3909 changed(); 3910 }); 3911 } 3912 3913 version(custom_widgets) 3914 override void recomputeChildLayout() { 3915 thumb.thumbWidth = this.width; 3916 super.recomputeChildLayout(); 3917 setPositionCustom(position_); 3918 } 3919 3920 version(custom_widgets) 3921 protected override void setPositionCustom(int a) { 3922 if(max()) 3923 thumb.positionY = (max - a) * (thumb.height - 16) / max(); 3924 redraw(); 3925 } 3926 } 3927 3928 /++ 3929 3930 +/ 3931 class HorizontalSlider : Slider { 3932 this(int min, int max, int step, Widget parent) { 3933 version(custom_widgets) 3934 initialize(); 3935 3936 super(min, max, step, parent); 3937 3938 version(win32_widgets) 3939 win32Setup(TBS_HORZ); 3940 } 3941 3942 version(win32_widgets) 3943 protected override void setPositionWindows(int a) { 3944 SendMessage(hwnd, TBM_SETPOS, true, a); 3945 } 3946 3947 protected override int win32direction() { 3948 return 1; 3949 } 3950 3951 version(custom_widgets) 3952 private void initialize() { 3953 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 3954 3955 thumb.tabStop = false; 3956 3957 thumb.thumbWidth = 16; 3958 thumb.thumbHeight = height; 3959 3960 thumb.addEventListener(EventType.change, () { 3961 auto sx = thumb.positionX * max() / (thumb.width - 16); 3962 //informProgramThatUserChangedPosition(sx); 3963 3964 position_ = sx; 3965 3966 changed(); 3967 }); 3968 } 3969 3970 version(custom_widgets) 3971 override void recomputeChildLayout() { 3972 thumb.thumbHeight = this.height; 3973 super.recomputeChildLayout(); 3974 setPositionCustom(position_); 3975 } 3976 3977 version(custom_widgets) 3978 protected override void setPositionCustom(int a) { 3979 if(max()) 3980 thumb.positionX = a * (thumb.width - 16) / max(); 3981 } 3982 } 3983 3984 3985 /// 3986 abstract class ScrollbarBase : Widget { 3987 /// 3988 this(Widget parent) { 3989 super(parent); 3990 tabStop = false; 3991 } 3992 3993 private int viewableArea_; 3994 private int max_; 3995 private int step_ = 16; 3996 private int position_; 3997 3998 /// 3999 bool atEnd() { 4000 return position_ + viewableArea_ >= max_; 4001 } 4002 4003 /// 4004 bool atStart() { 4005 return position_ == 0; 4006 } 4007 4008 /// 4009 void setViewableArea(int a) { 4010 viewableArea_ = a; 4011 version(custom_widgets) 4012 redraw(); 4013 } 4014 /// 4015 void setMax(int a) { 4016 max_ = a; 4017 version(custom_widgets) 4018 redraw(); 4019 } 4020 /// 4021 int max() { 4022 return max_; 4023 } 4024 /// 4025 void setPosition(int a) { 4026 if(a == int.max) 4027 a = max; 4028 position_ = max ? a : 0; 4029 if(position_ + viewableArea_ > max) 4030 position_ = max - viewableArea_; 4031 if(position_ < 0) 4032 position_ = 0; 4033 version(custom_widgets) 4034 redraw(); 4035 } 4036 /// 4037 int position() { 4038 return position_; 4039 } 4040 /// 4041 void setStep(int a) { 4042 step_ = a; 4043 } 4044 /// 4045 int step() { 4046 return step_; 4047 } 4048 4049 // FIXME: remove this.... maybe 4050 /+ 4051 protected void informProgramThatUserChangedPosition(int n) { 4052 position_ = n; 4053 auto evt = new Event(EventType.change, this); 4054 evt.intValue = n; 4055 evt.dispatch(); 4056 } 4057 +/ 4058 4059 version(custom_widgets) { 4060 abstract protected int getBarDim(); 4061 int thumbSize() { 4062 if(viewableArea_ >= max_) 4063 return getBarDim(); 4064 4065 int res; 4066 if(max_) { 4067 res = getBarDim() * viewableArea_ / max_; 4068 } 4069 if(res < 6) 4070 res = 6; 4071 4072 return res; 4073 } 4074 4075 int thumbPosition() { 4076 /* 4077 viewableArea_ is the viewport height/width 4078 position_ is where we are 4079 */ 4080 if(max_) { 4081 if(position_ + viewableArea_ >= max_) 4082 return getBarDim - thumbSize; 4083 return getBarDim * position_ / max_; 4084 } 4085 return 0; 4086 } 4087 } 4088 } 4089 4090 //public import mgt; 4091 4092 /++ 4093 A mouse tracking widget is one that follows the mouse when dragged inside it. 4094 4095 Concrete subclasses may include a scrollbar thumb and a volume control. 4096 +/ 4097 //version(custom_widgets) 4098 class MouseTrackingWidget : Widget { 4099 4100 /// 4101 int positionX() { return positionX_; } 4102 /// 4103 int positionY() { return positionY_; } 4104 4105 /// 4106 void positionX(int p) { positionX_ = p; } 4107 /// 4108 void positionY(int p) { positionY_ = p; } 4109 4110 private int positionX_; 4111 private int positionY_; 4112 4113 /// 4114 enum Orientation { 4115 horizontal, /// 4116 vertical, /// 4117 twoDimensional, /// 4118 } 4119 4120 private int thumbWidth_; 4121 private int thumbHeight_; 4122 4123 /// 4124 int thumbWidth() { return thumbWidth_; } 4125 /// 4126 int thumbHeight() { return thumbHeight_; } 4127 /// 4128 int thumbWidth(int a) { return thumbWidth_ = a; } 4129 /// 4130 int thumbHeight(int a) { return thumbHeight_ = a; } 4131 4132 private bool dragging; 4133 private bool hovering; 4134 private int startMouseX, startMouseY; 4135 4136 /// 4137 this(Orientation orientation, Widget parent = null) { 4138 super(parent); 4139 4140 //assert(parentWindow !is null); 4141 4142 addEventListener((MouseDownEvent event) { 4143 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 4144 dragging = true; 4145 startMouseX = event.clientX - positionX; 4146 startMouseY = event.clientY - positionY; 4147 parentWindow.captureMouse(this); 4148 } else { 4149 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 4150 positionX = event.clientX - thumbWidth / 2; 4151 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 4152 positionY = event.clientY - thumbHeight / 2; 4153 4154 if(positionX + thumbWidth > this.width) 4155 positionX = this.width - thumbWidth; 4156 if(positionY + thumbHeight > this.height) 4157 positionY = this.height - thumbHeight; 4158 4159 if(positionX < 0) 4160 positionX = 0; 4161 if(positionY < 0) 4162 positionY = 0; 4163 4164 4165 // this.emit!(ChangeEvent!void)(); 4166 auto evt = new Event(EventType.change, this); 4167 evt.sendDirectly(); 4168 4169 redraw(); 4170 4171 } 4172 }); 4173 4174 addEventListener(EventType.mouseup, (Event event) { 4175 dragging = false; 4176 parentWindow.releaseMouseCapture(); 4177 }); 4178 4179 addEventListener(EventType.mouseout, (Event event) { 4180 if(!hovering) 4181 return; 4182 hovering = false; 4183 redraw(); 4184 }); 4185 4186 int lpx, lpy; 4187 4188 addEventListener((MouseMoveEvent event) { 4189 auto oh = hovering; 4190 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 4191 hovering = true; 4192 } else { 4193 hovering = false; 4194 } 4195 if(!dragging) { 4196 if(hovering != oh) 4197 redraw(); 4198 return; 4199 } 4200 4201 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 4202 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 4203 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 4204 positionY = event.clientY - startMouseY; 4205 4206 if(positionX + thumbWidth > this.width) 4207 positionX = this.width - thumbWidth; 4208 if(positionY + thumbHeight > this.height) 4209 positionY = this.height - thumbHeight; 4210 4211 if(positionX < 0) 4212 positionX = 0; 4213 if(positionY < 0) 4214 positionY = 0; 4215 4216 if(positionX != lpx || positionY != lpy) { 4217 auto evt = new Event(EventType.change, this); 4218 evt.sendDirectly(); 4219 4220 lpx = positionX; 4221 lpy = positionY; 4222 } 4223 4224 redraw(); 4225 }); 4226 } 4227 4228 version(custom_widgets) 4229 override void paint(WidgetPainter painter) { 4230 auto cs = getComputedStyle(); 4231 auto c = darken(cs.windowBackgroundColor, 0.2); 4232 painter.outlineColor = c; 4233 painter.fillColor = c; 4234 painter.drawRectangle(Point(0, 0), this.width, this.height); 4235 4236 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 4237 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 4238 } 4239 } 4240 4241 //version(custom_widgets) 4242 //private 4243 class HorizontalScrollbar : ScrollbarBase { 4244 4245 version(custom_widgets) { 4246 private MouseTrackingWidget thumb; 4247 4248 override int getBarDim() { 4249 return thumb.width; 4250 } 4251 } 4252 4253 override void setViewableArea(int a) { 4254 super.setViewableArea(a); 4255 4256 version(win32_widgets) { 4257 SCROLLINFO info; 4258 info.cbSize = info.sizeof; 4259 info.nPage = a + 1; 4260 info.fMask = SIF_PAGE; 4261 SetScrollInfo(hwnd, SB_CTL, &info, true); 4262 } else version(custom_widgets) { 4263 thumb.positionX = thumbPosition; 4264 thumb.thumbWidth = thumbSize; 4265 thumb.redraw(); 4266 } else static assert(0); 4267 4268 } 4269 4270 override void setMax(int a) { 4271 super.setMax(a); 4272 version(win32_widgets) { 4273 SCROLLINFO info; 4274 info.cbSize = info.sizeof; 4275 info.nMin = 0; 4276 info.nMax = max; 4277 info.fMask = SIF_RANGE; 4278 SetScrollInfo(hwnd, SB_CTL, &info, true); 4279 } else version(custom_widgets) { 4280 thumb.positionX = thumbPosition; 4281 thumb.thumbWidth = thumbSize; 4282 thumb.redraw(); 4283 } 4284 } 4285 4286 override void setPosition(int a) { 4287 super.setPosition(a); 4288 version(win32_widgets) { 4289 SCROLLINFO info; 4290 info.cbSize = info.sizeof; 4291 info.fMask = SIF_POS; 4292 info.nPos = position; 4293 SetScrollInfo(hwnd, SB_CTL, &info, true); 4294 } else version(custom_widgets) { 4295 thumb.positionX = thumbPosition(); 4296 thumb.thumbWidth = thumbSize; 4297 thumb.redraw(); 4298 } else static assert(0); 4299 } 4300 4301 this(Widget parent) { 4302 super(parent); 4303 4304 version(win32_widgets) { 4305 createWin32Window(this, "Scrollbar"w, "", 4306 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 4307 } else version(custom_widgets) { 4308 auto vl = new HorizontalLayout(this); 4309 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 4310 leftButton.setClickRepeat(scrollClickRepeatInterval); 4311 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 4312 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 4313 rightButton.setClickRepeat(scrollClickRepeatInterval); 4314 4315 leftButton.tabStop = false; 4316 rightButton.tabStop = false; 4317 thumb.tabStop = false; 4318 4319 leftButton.addEventListener(EventType.triggered, () { 4320 this.emitCommand!"scrolltopreviousline"(); 4321 //informProgramThatUserChangedPosition(position - step()); 4322 }); 4323 rightButton.addEventListener(EventType.triggered, () { 4324 this.emitCommand!"scrolltonextline"(); 4325 //informProgramThatUserChangedPosition(position + step()); 4326 }); 4327 4328 thumb.thumbWidth = this.minWidth; 4329 thumb.thumbHeight = 16; 4330 4331 thumb.addEventListener(EventType.change, () { 4332 auto sx = thumb.positionX * max() / thumb.width; 4333 //informProgramThatUserChangedPosition(sx); 4334 4335 auto ev = new ScrollToPositionEvent(this, sx); 4336 ev.dispatch(); 4337 }); 4338 } 4339 } 4340 4341 override int minHeight() { return 16; } 4342 override int maxHeight() { return 16; } 4343 override int minWidth() { return 48; } 4344 } 4345 4346 class ScrollToPositionEvent : Event { 4347 this(Widget target, int value) { 4348 this.value = value; 4349 super("scrolltoposition", target); 4350 } 4351 4352 immutable int value; 4353 4354 override @property int intValue() { 4355 return value; 4356 } 4357 } 4358 4359 //version(custom_widgets) 4360 //private 4361 class VerticalScrollbar : ScrollbarBase { 4362 4363 version(custom_widgets) { 4364 override int getBarDim() { 4365 return thumb.height; 4366 } 4367 4368 private MouseTrackingWidget thumb; 4369 } 4370 4371 override void setViewableArea(int a) { 4372 super.setViewableArea(a); 4373 4374 version(win32_widgets) { 4375 SCROLLINFO info; 4376 info.cbSize = info.sizeof; 4377 info.nPage = a + 1; 4378 info.fMask = SIF_PAGE; 4379 SetScrollInfo(hwnd, SB_CTL, &info, true); 4380 } else version(custom_widgets) { 4381 thumb.positionY = thumbPosition; 4382 thumb.thumbHeight = thumbSize; 4383 thumb.redraw(); 4384 } else static assert(0); 4385 4386 } 4387 4388 override void setMax(int a) { 4389 super.setMax(a); 4390 version(win32_widgets) { 4391 SCROLLINFO info; 4392 info.cbSize = info.sizeof; 4393 info.nMin = 0; 4394 info.nMax = max; 4395 info.fMask = SIF_RANGE; 4396 SetScrollInfo(hwnd, SB_CTL, &info, true); 4397 } else version(custom_widgets) { 4398 thumb.positionY = thumbPosition; 4399 thumb.thumbHeight = thumbSize; 4400 thumb.redraw(); 4401 } 4402 } 4403 4404 override void setPosition(int a) { 4405 super.setPosition(a); 4406 version(win32_widgets) { 4407 SCROLLINFO info; 4408 info.cbSize = info.sizeof; 4409 info.fMask = SIF_POS; 4410 info.nPos = position; 4411 SetScrollInfo(hwnd, SB_CTL, &info, true); 4412 } else version(custom_widgets) { 4413 thumb.positionY = thumbPosition; 4414 thumb.thumbHeight = thumbSize; 4415 thumb.redraw(); 4416 } else static assert(0); 4417 } 4418 4419 this(Widget parent) { 4420 super(parent); 4421 4422 version(win32_widgets) { 4423 createWin32Window(this, "Scrollbar"w, "", 4424 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 4425 } else version(custom_widgets) { 4426 auto vl = new VerticalLayout(this); 4427 auto upButton = new ArrowButton(ArrowDirection.up, vl); 4428 upButton.setClickRepeat(scrollClickRepeatInterval); 4429 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 4430 auto downButton = new ArrowButton(ArrowDirection.down, vl); 4431 downButton.setClickRepeat(scrollClickRepeatInterval); 4432 4433 upButton.addEventListener(EventType.triggered, () { 4434 this.emitCommand!"scrolltopreviousline"(); 4435 //informProgramThatUserChangedPosition(position - step()); 4436 }); 4437 downButton.addEventListener(EventType.triggered, () { 4438 this.emitCommand!"scrolltonextline"(); 4439 //informProgramThatUserChangedPosition(position + step()); 4440 }); 4441 4442 thumb.thumbWidth = this.minWidth; 4443 thumb.thumbHeight = 16; 4444 4445 thumb.addEventListener(EventType.change, () { 4446 auto sy = thumb.positionY * max() / thumb.height; 4447 4448 auto ev = new ScrollToPositionEvent(this, sy); 4449 ev.dispatch(); 4450 4451 //informProgramThatUserChangedPosition(sy); 4452 }); 4453 4454 upButton.tabStop = false; 4455 downButton.tabStop = false; 4456 thumb.tabStop = false; 4457 } 4458 } 4459 4460 override int minWidth() { return 16; } 4461 override int maxWidth() { return 16; } 4462 override int minHeight() { return 48; } 4463 } 4464 4465 4466 4467 /// 4468 abstract class Layout : Widget { 4469 this(Widget parent = null) { 4470 tabStop = false; 4471 super(parent); 4472 } 4473 } 4474 4475 /++ 4476 Makes all children minimum width and height, placing them down 4477 left to right, top to bottom. 4478 4479 Useful if you want to make a list of buttons that automatically 4480 wrap to a new line when necessary. 4481 +/ 4482 class InlineBlockLayout : Layout { 4483 /// 4484 this(Widget parent = null) { super(parent); } 4485 4486 override void recomputeChildLayout() { 4487 registerMovement(); 4488 4489 int x = this.paddingLeft, y = this.paddingTop; 4490 4491 int lineHeight; 4492 int previousMargin = 0; 4493 int previousMarginBottom = 0; 4494 4495 foreach(child; children) { 4496 if(child.hidden) 4497 continue; 4498 if(cast(FixedPosition) child) { 4499 child.recomputeChildLayout(); 4500 continue; 4501 } 4502 child.width = child.minWidth(); 4503 if(child.width == 0) 4504 child.width = 32; 4505 child.height = child.minHeight(); 4506 if(child.height == 0) 4507 child.height = 32; 4508 4509 if(x + child.width + paddingRight > this.width) { 4510 x = this.paddingLeft; 4511 y += lineHeight; 4512 lineHeight = 0; 4513 previousMargin = 0; 4514 previousMarginBottom = 0; 4515 } 4516 4517 auto margin = child.marginLeft; 4518 if(previousMargin > margin) 4519 margin = previousMargin; 4520 4521 x += margin; 4522 4523 child.x = x; 4524 child.y = y; 4525 4526 int marginTopApplied; 4527 if(child.marginTop > previousMarginBottom) { 4528 child.y += child.marginTop; 4529 marginTopApplied = child.marginTop; 4530 } 4531 4532 x += child.width; 4533 previousMargin = child.marginRight; 4534 4535 if(child.marginBottom > previousMarginBottom) 4536 previousMarginBottom = child.marginBottom; 4537 4538 auto h = child.height + previousMarginBottom + marginTopApplied; 4539 if(h > lineHeight) 4540 lineHeight = h; 4541 4542 child.recomputeChildLayout(); 4543 } 4544 4545 } 4546 4547 override int minWidth() { 4548 int min; 4549 foreach(child; children) { 4550 auto cm = child.minWidth; 4551 if(cm > min) 4552 min = cm; 4553 } 4554 return min + paddingLeft + paddingRight; 4555 } 4556 4557 override int minHeight() { 4558 int min; 4559 foreach(child; children) { 4560 auto cm = child.minHeight; 4561 if(cm > min) 4562 min = cm; 4563 } 4564 return min + paddingTop + paddingBottom; 4565 } 4566 } 4567 4568 /++ 4569 A tab widget is a set of clickable tab buttons followed by a content area. 4570 4571 4572 Tabs can change existing content or can be new pages. 4573 4574 When the user picks a different tab, a `change` message is generated. 4575 +/ 4576 class TabWidget : Widget { 4577 this(Widget parent) { 4578 super(parent); 4579 4580 version(win32_widgets) { 4581 createWin32Window(this, WC_TABCONTROL, "", 0); 4582 } else version(custom_widgets) { 4583 addEventListener((ClickEvent event) { 4584 if(event.target !is this) return; 4585 if(event.clientY < tabBarHeight) { 4586 auto t = (event.clientX / tabWidth); 4587 if(t >= 0 && t < children.length) 4588 setCurrentTab(t); 4589 } 4590 }); 4591 } else static assert(0); 4592 } 4593 4594 override int marginTop() { return 4; } 4595 override int paddingBottom() { return 4; } 4596 4597 override int minHeight() { 4598 int max = 0; 4599 foreach(child; children) 4600 max = mymax(child.minHeight, max); 4601 4602 4603 version(win32_widgets) { 4604 RECT rect; 4605 rect.right = this.width; 4606 rect.bottom = max; 4607 TabCtrl_AdjustRect(hwnd, true, &rect); 4608 4609 max = rect.bottom; 4610 } else { 4611 max += Window.lineHeight + 4; 4612 } 4613 4614 4615 return max; 4616 } 4617 4618 version(win32_widgets) 4619 override int handleWmNotify(NMHDR* hdr, int code) { 4620 switch(code) { 4621 case TCN_SELCHANGE: 4622 auto sel = TabCtrl_GetCurSel(hwnd); 4623 showOnly(sel); 4624 break; 4625 default: 4626 } 4627 return 0; 4628 } 4629 4630 override void addChild(Widget child, int pos = int.max) { 4631 if(auto twp = cast(TabWidgetPage) child) { 4632 super.addChild(child, pos); 4633 if(pos == int.max) 4634 pos = cast(int) this.children.length - 1; 4635 4636 version(win32_widgets) { 4637 TCITEM item; 4638 item.mask = TCIF_TEXT; 4639 WCharzBuffer buf = WCharzBuffer(twp.title); 4640 item.pszText = buf.ptr; 4641 SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 4642 } else version(custom_widgets) { 4643 } 4644 4645 if(pos != getCurrentTab) { 4646 child.showing = false; 4647 } 4648 } else { 4649 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 4650 } 4651 } 4652 4653 override void recomputeChildLayout() { 4654 version(win32_widgets) { 4655 this.registerMovement(); 4656 4657 RECT rect; 4658 GetWindowRect(hwnd, &rect); 4659 4660 auto left = rect.left; 4661 auto top = rect.top; 4662 4663 TabCtrl_AdjustRect(hwnd, false, &rect); 4664 foreach(child; children) { 4665 child.x = rect.left - left; 4666 child.y = rect.top - top; 4667 child.width = rect.right - rect.left; 4668 child.height = rect.bottom - rect.top; 4669 child.recomputeChildLayout(); 4670 } 4671 } else version(custom_widgets) { 4672 this.registerMovement(); 4673 foreach(child; children) { 4674 child.x = 2; 4675 child.y = tabBarHeight + 2; // for the border 4676 child.width = width - 4; // for the border 4677 child.height = height - tabBarHeight - 2 - 2; // for the border 4678 child.recomputeChildLayout(); 4679 } 4680 } else static assert(0); 4681 } 4682 4683 version(custom_widgets) { 4684 private int currentTab_; 4685 private int tabBarHeight() { return Window.lineHeight; } 4686 int tabWidth = 80; 4687 } 4688 4689 version(custom_widgets) 4690 override void paint(WidgetPainter painter) { 4691 auto cs = getComputedStyle(); 4692 4693 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.backgroundColor); 4694 4695 int posX = 0; 4696 foreach(idx, child; children) { 4697 if(auto twp = cast(TabWidgetPage) child) { 4698 auto isCurrent = idx == getCurrentTab(); 4699 4700 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 4701 4702 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 4703 painter.outlineColor = cs.foregroundColor; 4704 painter.drawText(Point(posX + 4, 2), twp.title); 4705 4706 if(isCurrent) { 4707 painter.outlineColor = cs.windowBackgroundColor; 4708 painter.fillColor = Color.transparent; 4709 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 4710 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 4711 4712 painter.outlineColor = Color.white; 4713 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 4714 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 4715 painter.outlineColor = cs.activeTabColor; 4716 painter.drawPixel(Point(posX, tabBarHeight - 1)); 4717 } 4718 4719 posX += tabWidth - 2; 4720 } 4721 } 4722 } 4723 4724 /// 4725 @scriptable 4726 void setCurrentTab(int item) { 4727 version(win32_widgets) 4728 TabCtrl_SetCurSel(hwnd, item); 4729 else version(custom_widgets) 4730 currentTab_ = item; 4731 else static assert(0); 4732 4733 showOnly(item); 4734 } 4735 4736 /// 4737 @scriptable 4738 int getCurrentTab() { 4739 version(win32_widgets) 4740 return TabCtrl_GetCurSel(hwnd); 4741 else version(custom_widgets) 4742 return currentTab_; // FIXME 4743 else static assert(0); 4744 } 4745 4746 /// 4747 @scriptable 4748 void removeTab(int item) { 4749 if(item && item == getCurrentTab()) 4750 setCurrentTab(item - 1); 4751 4752 version(win32_widgets) { 4753 TabCtrl_DeleteItem(hwnd, item); 4754 } 4755 4756 for(int a = item; a < children.length - 1; a++) 4757 this._children[a] = this._children[a + 1]; 4758 this._children = this._children[0 .. $-1]; 4759 } 4760 4761 /// 4762 @scriptable 4763 TabWidgetPage addPage(string title) { 4764 return new TabWidgetPage(title, this); 4765 } 4766 4767 private void showOnly(int item) { 4768 foreach(idx, child; children) { 4769 child.hide(); 4770 } 4771 4772 foreach(idx, child; children) { 4773 if(idx == item) { 4774 child.show(); 4775 recomputeChildLayout(); 4776 } 4777 } 4778 4779 version(win32_widgets) { 4780 InvalidateRect(parentWindow.hwnd, null, true); 4781 } 4782 } 4783 } 4784 4785 /++ 4786 A page widget is basically a tab widget with hidden tabs. 4787 4788 You add [TabWidgetPage]s to it. 4789 +/ 4790 class PageWidget : Widget { 4791 this(Widget parent) { 4792 super(parent); 4793 } 4794 4795 override int minHeight() { 4796 int max = 0; 4797 foreach(child; children) 4798 max = mymax(child.minHeight, max); 4799 4800 return max; 4801 } 4802 4803 4804 override void addChild(Widget child, int pos = int.max) { 4805 if(auto twp = cast(TabWidgetPage) child) { 4806 super.addChild(child, pos); 4807 if(pos == int.max) 4808 pos = cast(int) this.children.length - 1; 4809 4810 if(pos != getCurrentTab) { 4811 child.showing = false; 4812 } 4813 } else { 4814 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 4815 } 4816 } 4817 4818 override void recomputeChildLayout() { 4819 this.registerMovement(); 4820 foreach(child; children) { 4821 child.x = 0; 4822 child.y = 0; 4823 child.width = width; 4824 child.height = height; 4825 child.recomputeChildLayout(); 4826 } 4827 } 4828 4829 private int currentTab_; 4830 4831 /// 4832 @scriptable 4833 void setCurrentTab(int item) { 4834 currentTab_ = item; 4835 4836 showOnly(item); 4837 } 4838 4839 /// 4840 @scriptable 4841 int getCurrentTab() { 4842 return currentTab_; 4843 } 4844 4845 /// 4846 @scriptable 4847 void removeTab(int item) { 4848 if(item && item == getCurrentTab()) 4849 setCurrentTab(item - 1); 4850 4851 for(int a = item; a < children.length - 1; a++) 4852 this._children[a] = this._children[a + 1]; 4853 this._children = this._children[0 .. $-1]; 4854 } 4855 4856 /// 4857 @scriptable 4858 TabWidgetPage addPage(string title) { 4859 return new TabWidgetPage(title, this); 4860 } 4861 4862 private void showOnly(int item) { 4863 foreach(idx, child; children) 4864 if(idx == item) { 4865 child.show(); 4866 child.recomputeChildLayout(); 4867 } else { 4868 child.hide(); 4869 } 4870 } 4871 4872 } 4873 4874 /++ 4875 4876 +/ 4877 class TabWidgetPage : Widget { 4878 string title; 4879 this(string title, Widget parent) { 4880 this.title = title; 4881 super(parent); 4882 4883 ///* 4884 version(win32_widgets) { 4885 static bool classRegistered = false; 4886 if(!classRegistered) { 4887 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 4888 WNDCLASSEX wc; 4889 wc.cbSize = wc.sizeof; 4890 wc.hInstance = hInstance; 4891 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 4892 wc.lpfnWndProc = &DefWindowProc; 4893 wc.lpszClassName = "arsd_minigui_TabWidgetPage"w.ptr; 4894 if(!RegisterClassExW(&wc)) 4895 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 4896 classRegistered = true; 4897 } 4898 4899 4900 createWin32Window(this, "arsd_minigui_TabWidgetPage"w, "", 0); 4901 } 4902 //*/ 4903 } 4904 4905 override int minHeight() { 4906 int sum = 0; 4907 foreach(child; children) 4908 sum += child.minHeight(); 4909 return sum; 4910 } 4911 } 4912 4913 version(none) 4914 class CollapsableSidebar : Widget { 4915 4916 } 4917 4918 /// Stacks the widgets vertically, taking all the available width for each child. 4919 class VerticalLayout : Layout { 4920 // intentionally blank - widget's default is vertical layout right now 4921 /// 4922 this(Widget parent) { super(parent); } 4923 } 4924 4925 /// Stacks the widgets horizontally, taking all the available height for each child. 4926 class HorizontalLayout : Layout { 4927 /// 4928 this(Widget parent = null) { super(parent); } 4929 override void recomputeChildLayout() { 4930 .recomputeChildLayout!"width"(this); 4931 } 4932 4933 override int minHeight() { 4934 int largest = 0; 4935 int margins = 0; 4936 int lastMargin = 0; 4937 foreach(child; children) { 4938 auto mh = child.minHeight(); 4939 if(mh > largest) 4940 largest = mh; 4941 margins += mymax(lastMargin, child.marginTop()); 4942 lastMargin = child.marginBottom(); 4943 } 4944 return largest + margins; 4945 } 4946 4947 override int maxHeight() { 4948 int largest = 0; 4949 int margins = 0; 4950 int lastMargin = 0; 4951 foreach(child; children) { 4952 auto mh = child.maxHeight(); 4953 if(mh == int.max) 4954 return int.max; 4955 if(mh > largest) 4956 largest = mh; 4957 margins += mymax(lastMargin, child.marginTop()); 4958 lastMargin = child.marginBottom(); 4959 } 4960 return largest + margins; 4961 } 4962 4963 override int heightStretchiness() { 4964 int max; 4965 foreach(child; children) { 4966 auto c = child.heightStretchiness; 4967 if(c > max) 4968 max = c; 4969 } 4970 return max; 4971 } 4972 4973 } 4974 4975 /++ 4976 A widget that takes your widget, puts scroll bars around it, and sends 4977 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 4978 no effort to automatically scroll or clip its child widgets - it just sends 4979 the messages. 4980 +/ 4981 class ScrollMessageWidget : Widget { 4982 this(Widget parent = null) { 4983 super(parent); 4984 4985 container = new Widget(this); 4986 hsb = new HorizontalScrollbar(this); 4987 vsb = new VerticalScrollbar(this); 4988 4989 hsb.addEventListener("scrolltonextline", { 4990 hsb.setPosition(hsb.position + 1); 4991 notify(); 4992 }); 4993 hsb.addEventListener("scrolltopreviousline", { 4994 hsb.setPosition(hsb.position - 1); 4995 notify(); 4996 }); 4997 vsb.addEventListener("scrolltonextline", { 4998 vsb.setPosition(vsb.position + 1); 4999 notify(); 5000 }); 5001 vsb.addEventListener("scrolltopreviousline", { 5002 vsb.setPosition(vsb.position - 1); 5003 notify(); 5004 }); 5005 hsb.addEventListener("scrolltonextpage", { 5006 hsb.setPosition(hsb.position + hsb.step_); 5007 notify(); 5008 }); 5009 hsb.addEventListener("scrolltopreviouspage", { 5010 hsb.setPosition(hsb.position - hsb.step_); 5011 notify(); 5012 }); 5013 vsb.addEventListener("scrolltonextpage", { 5014 vsb.setPosition(vsb.position + vsb.step_); 5015 notify(); 5016 }); 5017 vsb.addEventListener("scrolltopreviouspage", { 5018 vsb.setPosition(vsb.position - vsb.step_); 5019 notify(); 5020 }); 5021 hsb.addEventListener("scrolltoposition", (Event event) { 5022 hsb.setPosition(event.intValue); 5023 notify(); 5024 }); 5025 vsb.addEventListener("scrolltoposition", (Event event) { 5026 vsb.setPosition(event.intValue); 5027 notify(); 5028 }); 5029 5030 5031 tabStop = false; 5032 container.tabStop = false; 5033 magic = true; 5034 } 5035 5036 /// 5037 void scrollUp() { 5038 vsb.setPosition(vsb.position - 1); 5039 notify(); 5040 } 5041 /// Ditto 5042 void scrollDown() { 5043 vsb.setPosition(vsb.position + 1); 5044 notify(); 5045 } 5046 5047 /// 5048 VerticalScrollbar verticalScrollBar() { return vsb; } 5049 /// 5050 HorizontalScrollbar horizontalScrollBar() { return hsb; } 5051 5052 void notify() { 5053 this.emit!ScrollEvent(); 5054 } 5055 5056 mixin Emits!ScrollEvent; 5057 5058 /// 5059 Point position() { 5060 return Point(hsb.position, vsb.position); 5061 } 5062 5063 /// 5064 void setPosition(int x, int y) { 5065 hsb.setPosition(x); 5066 vsb.setPosition(y); 5067 } 5068 5069 /// 5070 void setPageSize(int unitsX, int unitsY) { 5071 hsb.setStep(unitsX); 5072 vsb.setStep(unitsY); 5073 } 5074 5075 /// 5076 void setTotalArea(int width, int height) { 5077 hsb.setMax(width); 5078 vsb.setMax(height); 5079 } 5080 5081 /// 5082 void setViewableArea(int width, int height) { 5083 hsb.setViewableArea(width); 5084 vsb.setViewableArea(height); 5085 } 5086 5087 private bool magic; 5088 override void addChild(Widget w, int position = int.max) { 5089 if(magic) 5090 container.addChild(w, position); 5091 else 5092 super.addChild(w, position); 5093 } 5094 5095 override void recomputeChildLayout() { 5096 if(hsb is null || vsb is null || container is null) return; 5097 5098 registerMovement(); 5099 5100 hsb.height = 16; // FIXME? are tese 16s sane? 5101 hsb.x = 0; 5102 hsb.y = this.height - hsb.height; 5103 hsb.width = this.width - 16; 5104 hsb.recomputeChildLayout(); 5105 5106 vsb.width = 16; // FIXME? 5107 vsb.x = this.width - vsb.width; 5108 vsb.y = 0; 5109 vsb.height = this.height - 16; 5110 vsb.recomputeChildLayout(); 5111 5112 container.x = 0; 5113 container.y = 0; 5114 container.width = this.width - vsb.width; 5115 container.height = this.height - hsb.height; 5116 container.recomputeChildLayout(); 5117 } 5118 5119 HorizontalScrollbar hsb; 5120 VerticalScrollbar vsb; 5121 Widget container; 5122 } 5123 5124 /++ 5125 Bypasses automatic layout for its children, using manual positioning and sizing only. 5126 While you need to manually position them, you must ensure they are inside the StaticLayout's 5127 bounding box to avoid undefined behavior. 5128 5129 You should almost never use this. 5130 +/ 5131 class StaticLayout : Layout { 5132 /// 5133 this(Widget parent = null) { super(parent); } 5134 override void recomputeChildLayout() { 5135 registerMovement(); 5136 foreach(child; children) 5137 child.recomputeChildLayout(); 5138 } 5139 } 5140 5141 /++ 5142 Bypasses automatic positioning when being laid out. It is your responsibility to make 5143 room for this widget in the parent layout. 5144 5145 Its children are laid out normally, unless there is exactly one, in which case it takes 5146 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 5147 can do that with `padding`). 5148 +/ 5149 class StaticPosition : Layout { 5150 /// 5151 this(Widget parent = null) { super(parent); } 5152 5153 override void recomputeChildLayout() { 5154 registerMovement(); 5155 if(this.children.length == 1) { 5156 auto child = children[0]; 5157 child.x = 0; 5158 child.y = 0; 5159 child.width = this.width; 5160 child.height = this.height; 5161 child.recomputeChildLayout(); 5162 } else 5163 foreach(child; children) 5164 child.recomputeChildLayout(); 5165 } 5166 5167 } 5168 5169 /++ 5170 FixedPosition is like [StaticPosition], but its coordinates 5171 are always relative to the viewport, meaning they do not scroll with 5172 the parent content. 5173 +/ 5174 class FixedPosition : StaticPosition { 5175 /// 5176 this(Widget parent) { super(parent); } 5177 } 5178 5179 version(win32_widgets) 5180 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 5181 if(true) { 5182 // cmd == 0 = menu, cmd == 1 = accelerator 5183 if(auto item = idm in Action.mapping) { 5184 foreach(handler; (*item).triggered) 5185 handler(); 5186 /* 5187 auto event = new Event("triggered", *item); 5188 event.button = idm; 5189 event.dispatch(); 5190 */ 5191 return 0; 5192 } 5193 } 5194 if(handle) 5195 if(auto widgetp = handle in Widget.nativeMapping) { 5196 (*widgetp).handleWmCommand(cmd, idm); 5197 return 0; 5198 } 5199 return 1; 5200 } 5201 5202 5203 /// 5204 class Window : Widget { 5205 int mouseCaptureCount = 0; 5206 Widget mouseCapturedBy; 5207 void captureMouse(Widget byWhom) { 5208 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 5209 mouseCaptureCount++; 5210 mouseCapturedBy = byWhom; 5211 win.grabInput(); 5212 } 5213 void releaseMouseCapture() { 5214 mouseCaptureCount--; 5215 mouseCapturedBy = null; 5216 win.releaseInputGrab(); 5217 } 5218 5219 /// 5220 @scriptable 5221 @property bool focused() { 5222 return win.focused; 5223 } 5224 5225 static class Style : Widget.Style { 5226 override Color backgroundColor() { 5227 version(custom_widgets) 5228 return WidgetPainter.visualTheme.windowBackgroundColor; 5229 else version(win32_widgets) 5230 return Color.transparent; 5231 else static assert(0); 5232 } 5233 } 5234 mixin OverrideStyle!Style; 5235 5236 /++ 5237 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. 5238 +/ 5239 static int lineHeight() { 5240 OperatingSystemFont font; 5241 if(auto vt = WidgetPainter.visualTheme) { 5242 font = vt.defaultFontCached(); 5243 } 5244 5245 if(font is null) { 5246 static int defaultHeightCache; 5247 if(defaultHeightCache == 0) { 5248 font = new OperatingSystemFont; 5249 font.loadDefault; 5250 defaultHeightCache = font.height() * 5 / 4; 5251 } 5252 return defaultHeightCache; 5253 } 5254 5255 return font.height() * 5 / 4; 5256 } 5257 5258 Widget focusedWidget; 5259 5260 SimpleWindow win; 5261 5262 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 5263 this(Widget p) { 5264 tabStop = false; 5265 super(p); 5266 } 5267 5268 private bool skipNextChar = false; 5269 5270 /// 5271 this(SimpleWindow win) { 5272 5273 static if(UsingSimpledisplayX11) { 5274 win.discardAdditionalConnectionState = &discardXConnectionState; 5275 win.recreateAdditionalConnectionState = &recreateXConnectionState; 5276 } 5277 5278 tabStop = false; 5279 super(null); 5280 this.win = win; 5281 5282 win.addEventListener((Widget.RedrawEvent) { 5283 //import std.stdio; writeln("redrawing"); 5284 this.actualRedraw(); 5285 }); 5286 5287 this.width = win.width; 5288 this.height = win.height; 5289 this.parentWindow = this; 5290 5291 win.windowResized = (int w, int h) { 5292 this.width = w; 5293 this.height = h; 5294 recomputeChildLayout(); 5295 version(win32_widgets) 5296 InvalidateRect(hwnd, null, true); 5297 redraw(); 5298 }; 5299 5300 win.onFocusChange = (bool getting) { 5301 if(this.focusedWidget) { 5302 if(getting) 5303 this.focusedWidget.emit!FocusEvent(); 5304 else 5305 this.focusedWidget.emit!BlurEvent(); 5306 } 5307 5308 if(getting) 5309 this.emit!FocusEvent(); 5310 else 5311 this.emit!BlurEvent(); 5312 }; 5313 5314 win.setEventHandlers( 5315 (MouseEvent e) { 5316 dispatchMouseEvent(e); 5317 }, 5318 (KeyEvent e) { 5319 //import std.stdio; 5320 //writefln("%x %s", cast(uint) e.key, e.key); 5321 dispatchKeyEvent(e); 5322 }, 5323 (dchar e) { 5324 if(e == 13) e = 10; // hack? 5325 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 5326 dispatchCharEvent(e); 5327 }, 5328 ); 5329 5330 addEventListener("char", (Widget, Event ev) { 5331 if(skipNextChar) { 5332 ev.preventDefault(); 5333 skipNextChar = false; 5334 } 5335 }); 5336 5337 version(win32_widgets) 5338 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 5339 5340 if(hwnd !is this.win.impl.hwnd) 5341 return 1; // we don't care... 5342 switch(msg) { 5343 5344 case WM_VSCROLL, WM_HSCROLL: 5345 auto pos = HIWORD(wParam); 5346 auto m = LOWORD(wParam); 5347 5348 auto scrollbarHwnd = cast(HWND) lParam; 5349 5350 5351 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 5352 5353 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 5354 5355 switch(m) { 5356 /+ 5357 // I don't think those messages are ever actually sent normally by the widget itself, 5358 // they are more used for the keyboard interface. methinks. 5359 case SB_BOTTOM: 5360 import std.stdio; writeln("end"); 5361 auto event = new Event("scrolltoend", *widgetp); 5362 event.dispatch(); 5363 //if(!event.defaultPrevented) 5364 break; 5365 case SB_TOP: 5366 import std.stdio; writeln("top"); 5367 auto event = new Event("scrolltobeginning", *widgetp); 5368 event.dispatch(); 5369 break; 5370 case SB_ENDSCROLL: 5371 // idk 5372 break; 5373 +/ 5374 case SB_LINEDOWN: 5375 this.emitCommand!"scrolltonextline"(); 5376 break; 5377 case SB_LINEUP: 5378 this.emitCommand!"scrolltopreviousline"(); 5379 break; 5380 case SB_PAGEDOWN: 5381 this.emitCommand!"scrolltonextpage"(); 5382 break; 5383 case SB_PAGEUP: 5384 this.emitCommand!"scrolltopreviouspage"(); 5385 break; 5386 case SB_THUMBPOSITION: 5387 auto ev = new ScrollToPositionEvent(*widgetp, pos); 5388 ev.dispatch(); 5389 break; 5390 case SB_THUMBTRACK: 5391 // eh kinda lying but i like the real time update display 5392 auto ev = new ScrollToPositionEvent(*widgetp, pos); 5393 ev.dispatch(); 5394 // the event loop doesn't seem to carry on with a requested redraw.. 5395 // so we request it to get our dirty bit set... 5396 // then we need to immediately actually redraw it too for instant feedback to user 5397 if(redrawRequested) 5398 actualRedraw(); 5399 break; 5400 default: 5401 } 5402 } else { 5403 return 1; 5404 } 5405 break; 5406 5407 case WM_CONTEXTMENU: 5408 auto hwndFrom = cast(HWND) wParam; 5409 5410 auto xPos = cast(short) LOWORD(lParam); 5411 auto yPos = cast(short) HIWORD(lParam); 5412 5413 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 5414 POINT p; 5415 p.x = xPos; 5416 p.y = yPos; 5417 ScreenToClient(hwnd, &p); 5418 auto clientX = cast(ushort) p.x; 5419 auto clientY = cast(ushort) p.y; 5420 5421 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 5422 5423 if(!wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) 5424 return 1; // it didn't show above, pass message on 5425 } 5426 break; 5427 5428 case WM_NOTIFY: 5429 auto hdr = cast(NMHDR*) lParam; 5430 auto hwndFrom = hdr.hwndFrom; 5431 auto code = hdr.code; 5432 5433 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 5434 return (*widgetp).handleWmNotify(hdr, code); 5435 } 5436 break; 5437 case WM_COMMAND: 5438 auto handle = cast(HWND) lParam; 5439 auto cmd = HIWORD(wParam); 5440 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 5441 5442 default: return 1; // not handled, pass it on 5443 } 5444 return 0; 5445 }; 5446 } 5447 5448 version(win32_widgets) 5449 override void paint(WidgetPainter painter) { 5450 /* 5451 RECT rect; 5452 rect.right = this.width; 5453 rect.bottom = this.height; 5454 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 5455 */ 5456 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 5457 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5458 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5459 // since the pen is null, to fill the whole space, we need the +1 on both. 5460 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5461 SelectObject(painter.impl.hdc, p); 5462 SelectObject(painter.impl.hdc, b); 5463 } 5464 version(custom_widgets) 5465 override void paint(WidgetPainter painter) { 5466 auto cs = getComputedStyle(); 5467 painter.fillColor = cs.windowBackgroundColor; 5468 painter.outlineColor = cs.windowBackgroundColor; 5469 painter.drawRectangle(Point(0, 0), this.width, this.height); 5470 } 5471 5472 5473 override void defaultEventHandler_keydown(KeyDownEvent event) { 5474 Widget _this = event.target; 5475 5476 if(event.key == Key.Tab) { 5477 /* Window tab ordering is a recursive thingy with each group */ 5478 5479 // FIXME inefficient 5480 Widget[] helper(Widget p) { 5481 if(p.hidden) 5482 return null; 5483 Widget[] childOrdering; 5484 5485 auto children = p.children.dup; 5486 5487 while(true) { 5488 // UIs should be generally small, so gonna brute force it a little 5489 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 5490 5491 Widget smallestTab; 5492 foreach(ref c; children) { 5493 if(c is null) continue; 5494 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 5495 smallestTab = c; 5496 c = null; 5497 } 5498 } 5499 if(smallestTab !is null) { 5500 if(smallestTab.tabStop && !smallestTab.hidden) 5501 childOrdering ~= smallestTab; 5502 if(!smallestTab.hidden) 5503 childOrdering ~= helper(smallestTab); 5504 } else 5505 break; 5506 5507 } 5508 5509 return childOrdering; 5510 } 5511 5512 Widget[] tabOrdering = helper(this); 5513 5514 Widget recipient; 5515 5516 if(tabOrdering.length) { 5517 bool seenThis = false; 5518 Widget previous; 5519 foreach(idx, child; tabOrdering) { 5520 if(child is focusedWidget) { 5521 5522 if(event.shiftKey) { 5523 if(idx == 0) 5524 recipient = tabOrdering[$-1]; 5525 else 5526 recipient = tabOrdering[idx - 1]; 5527 break; 5528 } 5529 5530 seenThis = true; 5531 if(idx + 1 == tabOrdering.length) { 5532 // we're at the end, either move to the next group 5533 // or start back over 5534 recipient = tabOrdering[0]; 5535 } 5536 continue; 5537 } 5538 if(seenThis) { 5539 recipient = child; 5540 break; 5541 } 5542 previous = child; 5543 } 5544 } 5545 5546 if(recipient !is null) { 5547 // import std.stdio; writeln(typeid(recipient)); 5548 recipient.focus(); 5549 5550 skipNextChar = true; 5551 } 5552 } 5553 5554 debug if(event.key == Key.F12) { 5555 if(devTools) { 5556 devTools.close(); 5557 devTools = null; 5558 } else { 5559 devTools = new DevToolWindow(this); 5560 devTools.show(); 5561 } 5562 } 5563 } 5564 5565 debug DevToolWindow devTools; 5566 5567 5568 /++ 5569 History: 5570 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is Runtime.args[0] instead. 5571 +/ 5572 this(int width = 500, int height = 500, string title = null) { 5573 if(title is null) { 5574 import core.runtime; 5575 if(Runtime.args.length) 5576 title = Runtime.args[0]; 5577 } 5578 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow); 5579 this(win); 5580 } 5581 5582 /// 5583 this(string title) { 5584 this(500, 500, title); 5585 } 5586 5587 /// 5588 @scriptable 5589 void close() { 5590 win.close(); 5591 // I synchronize here upon window closing to ensure all child windows 5592 // get updated too before the event loop. This avoids some random X errors. 5593 static if(UsingSimpledisplayX11) 5594 XSync(XDisplayConnection.get, false); 5595 } 5596 5597 bool dispatchKeyEvent(KeyEvent ev) { 5598 auto wid = focusedWidget; 5599 if(wid is null) 5600 wid = this; 5601 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 5602 event.originalKeyEvent = ev; 5603 event.key = ev.key; 5604 event.state = ev.modifierState; 5605 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 5606 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 5607 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 5608 event.dispatch(); 5609 5610 return true; 5611 } 5612 5613 bool dispatchCharEvent(dchar ch) { 5614 if(focusedWidget) { 5615 auto event = new CharEvent(focusedWidget, ch); 5616 event.dispatch(); 5617 } 5618 return true; 5619 } 5620 5621 Widget mouseLastOver; 5622 Widget mouseLastDownOn; 5623 bool lastWasDoubleClick; 5624 bool dispatchMouseEvent(MouseEvent ev) { 5625 auto eleR = widgetAtPoint(this, ev.x, ev.y); 5626 auto ele = eleR.widget; 5627 5628 if(mouseCapturedBy !is null) { 5629 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 5630 ele = mouseCapturedBy; 5631 } 5632 5633 // a hack to get it relative to the widget. 5634 eleR.x = ev.x; 5635 eleR.y = ev.y; 5636 auto pain = ele; 5637 while(pain) { 5638 eleR.x -= pain.x; 5639 eleR.y -= pain.y; 5640 pain = pain.parent; 5641 } 5642 5643 if(ev.type == MouseEventType.buttonPressed) { 5644 MouseEventBase event = new MouseDownEvent(ele); 5645 event.button = ev.button; 5646 event.buttonLinear = ev.buttonLinear; 5647 event.state = ev.modifierState; 5648 event.clientX = eleR.x; 5649 event.clientY = eleR.y; 5650 event.dispatch(); 5651 5652 if(mouseLastDownOn is ele && ev.doubleClick) { 5653 event = new DoubleClickEvent(ele); 5654 event.button = ev.button; 5655 event.buttonLinear = ev.buttonLinear; 5656 event.state = ev.modifierState; 5657 event.clientX = eleR.x; 5658 event.clientY = eleR.y; 5659 event.dispatch(); 5660 lastWasDoubleClick = ev.doubleClick; 5661 } else { 5662 lastWasDoubleClick = false; 5663 } 5664 5665 mouseLastDownOn = ele; 5666 } else if(ev.type == MouseEventType.buttonReleased) { 5667 { 5668 auto event = new MouseUpEvent(ele); 5669 event.button = ev.button; 5670 event.buttonLinear = ev.buttonLinear; 5671 event.clientX = eleR.x; 5672 event.clientY = eleR.y; 5673 event.state = ev.modifierState; 5674 event.dispatch(); 5675 } 5676 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 5677 MouseEventBase event = new ClickEvent(ele); 5678 event.clientX = eleR.x; 5679 event.clientY = eleR.y; 5680 event.state = ev.modifierState; 5681 event.button = ev.button; 5682 event.buttonLinear = ev.buttonLinear; 5683 event.dispatch(); 5684 } 5685 } else if(ev.type == MouseEventType.motion) { 5686 // motion 5687 { 5688 auto event = new MouseMoveEvent(ele); 5689 event.state = ev.modifierState; 5690 event.clientX = eleR.x; 5691 event.clientY = eleR.y; 5692 event.dispatch(); 5693 } 5694 5695 if(mouseLastOver !is ele) { 5696 if(ele !is null) { 5697 if(!isAParentOf(ele, mouseLastOver)) { 5698 ele.setDynamicState(DynamicState.hover, true); 5699 auto event = new MouseEnterEvent(ele); 5700 event.relatedTarget = mouseLastOver; 5701 event.sendDirectly(); 5702 5703 ele.useStyleProperties((s) { 5704 ele.parentWindow.win.cursor = s.cursor; 5705 }); 5706 } 5707 } 5708 5709 if(mouseLastOver !is null) { 5710 if(!isAParentOf(mouseLastOver, ele)) { 5711 mouseLastOver.setDynamicState(DynamicState.hover, false); 5712 auto event = new MouseLeaveEvent(mouseLastOver); 5713 event.relatedTarget = ele; 5714 event.sendDirectly(); 5715 } 5716 } 5717 5718 if(ele !is null) { 5719 auto event = new MouseOverEvent(ele); 5720 event.relatedTarget = mouseLastOver; 5721 event.dispatch(); 5722 } 5723 5724 if(mouseLastOver !is null) { 5725 auto event = new MouseOutEvent(mouseLastOver); 5726 event.relatedTarget = ele; 5727 event.dispatch(); 5728 } 5729 5730 mouseLastOver = ele; 5731 } 5732 } 5733 5734 return true; 5735 } 5736 5737 /// Shows the window and runs the application event loop. 5738 @scriptable 5739 void loop() { 5740 show(); 5741 win.eventLoop(0); 5742 } 5743 5744 private bool firstShow = true; 5745 5746 @scriptable 5747 override void show() { 5748 bool rd = false; 5749 if(firstShow) { 5750 firstShow = false; 5751 recomputeChildLayout(); 5752 auto f = getFirstFocusable(this); // FIXME: autofocus? 5753 if(f) 5754 f.focus(); 5755 redraw(); 5756 } 5757 win.show(); 5758 super.show(); 5759 } 5760 @scriptable 5761 override void hide() { 5762 win.hide(); 5763 super.hide(); 5764 } 5765 5766 static Widget getFirstFocusable(Widget start) { 5767 if(start.tabStop && !start.hidden) 5768 return start; 5769 5770 if(!start.hidden) 5771 foreach(child; start.children) { 5772 auto f = getFirstFocusable(child); 5773 if(f !is null) 5774 return f; 5775 } 5776 return null; 5777 } 5778 } 5779 5780 debug private class DevToolWindow : Window { 5781 Window p; 5782 5783 TextEdit parentList; 5784 TextEdit logWindow; 5785 TextLabel clickX, clickY; 5786 5787 this(Window p) { 5788 this.p = p; 5789 super(400, 300, "Developer Toolbox"); 5790 5791 logWindow = new TextEdit(this); 5792 parentList = new TextEdit(this); 5793 5794 auto hl = new HorizontalLayout(this); 5795 clickX = new TextLabel("", hl); 5796 clickY = new TextLabel("", hl); 5797 5798 parentListeners ~= p.addEventListener("*", (Event ev) { 5799 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 5800 }); 5801 5802 parentListeners ~= p.addEventListener((ClickEvent ev) { 5803 auto s = ev.srcElement; 5804 string list = s.toString(); 5805 s = s.parent; 5806 while(s) { 5807 list ~= "\n"; 5808 list ~= s.toString(); 5809 s = s.parent; 5810 } 5811 parentList.content = list; 5812 5813 clickX.label = toInternal!string(ev.clientX); 5814 clickY.label = toInternal!string(ev.clientY); 5815 }); 5816 } 5817 5818 EventListener[] parentListeners; 5819 5820 override void close() { 5821 assert(p !is null); 5822 foreach(p; parentListeners) 5823 p.disconnect(); 5824 parentListeners = null; 5825 p.devTools = null; 5826 p = null; 5827 super.close(); 5828 } 5829 5830 override void defaultEventHandler_keydown(KeyDownEvent ev) { 5831 if(ev.key == Key.F12) { 5832 this.close(); 5833 if(p) 5834 p.devTools = null; 5835 } else { 5836 super.defaultEventHandler_keydown(ev); 5837 } 5838 } 5839 5840 void log(T...)(T t) { 5841 string str; 5842 import std.conv; 5843 foreach(i; t) 5844 str ~= to!string(i); 5845 str ~= "\n"; 5846 logWindow.addText(str); 5847 5848 version(custom_widgets) 5849 logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 5850 } 5851 } 5852 5853 /++ 5854 A dialog is a transient window that intends to get information from 5855 the user before being dismissed. 5856 +/ 5857 abstract class Dialog : Window { 5858 /// 5859 this(int width, int height, string title = null) { 5860 super(width, height, title); 5861 } 5862 5863 /// 5864 abstract void OK(); 5865 5866 /// 5867 void Cancel() { 5868 this.close(); 5869 } 5870 } 5871 5872 /// 5873 class LabeledLineEdit : Widget { 5874 /// 5875 this(string label, Widget parent = null) { 5876 super(parent); 5877 tabStop = false; 5878 auto hl = new HorizontalLayout(this); 5879 this.label = new TextLabel(label, hl); 5880 this.lineEdit = new LineEdit(hl); 5881 } 5882 TextLabel label; /// 5883 LineEdit lineEdit; /// 5884 5885 override int minHeight() { return Window.lineHeight + 4; } 5886 override int maxHeight() { return Window.lineHeight + 4; } 5887 5888 /// 5889 string content() { 5890 return lineEdit.content; 5891 } 5892 /// 5893 void content(string c) { 5894 return lineEdit.content(c); 5895 } 5896 5897 /// 5898 void selectAll() { 5899 lineEdit.selectAll(); 5900 } 5901 5902 override void focus() { 5903 lineEdit.focus(); 5904 } 5905 } 5906 5907 /++ 5908 A labeled password edit. 5909 5910 History: 5911 Added January 25, 2021 5912 5913 Future_Directions: 5914 This might be merged with [LabeledLineEdit] at some point. If I do that in code, I'll 5915 alias the names or something. 5916 +/ 5917 class LabeledPasswordEdit : Widget { 5918 /// 5919 this(string label = "Password: ", Widget parent = null) { 5920 super(parent); 5921 tabStop = false; 5922 auto hl = new HorizontalLayout(this); 5923 this.label = new TextLabel(label, hl); 5924 this.lineEdit = new PasswordEdit(hl); 5925 } 5926 TextLabel label; /// 5927 PasswordEdit lineEdit; /// 5928 5929 override int minHeight() { return Window.lineHeight + 4; } 5930 override int maxHeight() { return Window.lineHeight + 4; } 5931 5932 /// 5933 string content() { 5934 return lineEdit.content; 5935 } 5936 /// 5937 void content(string c) { 5938 return lineEdit.content(c); 5939 } 5940 5941 /// 5942 void selectAll() { 5943 lineEdit.selectAll(); 5944 } 5945 5946 override void focus() { 5947 lineEdit.focus(); 5948 } 5949 } 5950 5951 5952 /// 5953 class MainWindow : Window { 5954 /// 5955 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 5956 super(initialWidth, initialHeight, title); 5957 5958 _clientArea = new ClientAreaWidget(); 5959 _clientArea.x = 0; 5960 _clientArea.y = 0; 5961 _clientArea.width = this.width; 5962 _clientArea.height = this.height; 5963 _clientArea.tabStop = false; 5964 5965 super.addChild(_clientArea); 5966 5967 statusBar = new StatusBar(this); 5968 } 5969 5970 /++ 5971 Adds a menu and toolbar from annotated functions. 5972 5973 --- 5974 struct Commands { 5975 @menu("File") { 5976 void New() {} 5977 void Open() {} 5978 void Save() {} 5979 @separator 5980 void Exit() @accelerator("Alt+F4") { 5981 window.close(); 5982 } 5983 } 5984 5985 @menu("Edit") { 5986 void Undo() { 5987 undo(); 5988 } 5989 @separator 5990 void Cut() {} 5991 void Copy() {} 5992 void Paste() {} 5993 } 5994 5995 @menu("Help") { 5996 void About() {} 5997 } 5998 } 5999 6000 Commands commands; 6001 6002 window.setMenuAndToolbarFromAnnotatedCode(commands); 6003 --- 6004 6005 +/ 6006 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 6007 setMenuAndToolbarFromAnnotatedCode_internal(t); 6008 } 6009 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 6010 setMenuAndToolbarFromAnnotatedCode_internal(t); 6011 } 6012 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 6013 Action[] toolbarActions; 6014 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 6015 Menu[string] mcs; 6016 6017 foreach(menu; menuBar.subMenus) { 6018 mcs[menu.label] = menu; 6019 } 6020 6021 void delegate() triggering; 6022 6023 foreach(memberName; __traits(derivedMembers, T)) { 6024 static if(__traits(compiles, triggering = &__traits(getMember, t, memberName))) { 6025 .menu menu; 6026 .toolbar toolbar; 6027 bool separator; 6028 .accelerator accelerator; 6029 .hotkey hotkey; 6030 .icon icon; 6031 string label; 6032 string tip; 6033 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 6034 static if(is(typeof(attr) == .menu)) 6035 menu = attr; 6036 else static if(is(typeof(attr) == .toolbar)) 6037 toolbar = attr; 6038 else static if(is(attr == .separator)) 6039 separator = true; 6040 else static if(is(typeof(attr) == .accelerator)) 6041 accelerator = attr; 6042 else static if(is(typeof(attr) == .hotkey)) 6043 hotkey = attr; 6044 else static if(is(typeof(attr) == .icon)) 6045 icon = attr; 6046 else static if(is(typeof(attr) == .label)) 6047 label = attr.label; 6048 else static if(is(typeof(attr) == .tip)) 6049 tip = attr.tip; 6050 } 6051 6052 if(menu !is .menu.init || toolbar !is .toolbar.init) { 6053 ushort correctIcon = icon.id; // FIXME 6054 if(label.length == 0) 6055 label = memberName; 6056 auto action = new Action(label, correctIcon, &__traits(getMember, t, memberName)); 6057 6058 if(accelerator.keyString.length) { 6059 auto ke = KeyEvent.parse(accelerator.keyString); 6060 action.accelerator = ke; 6061 accelerators[ke.toStr] = &__traits(getMember, t, memberName); 6062 } 6063 6064 if(toolbar !is .toolbar.init) 6065 toolbarActions ~= action; 6066 if(menu !is .menu.init) { 6067 Menu mc; 6068 if(menu.name in mcs) { 6069 mc = mcs[menu.name]; 6070 } else { 6071 mc = new Menu(menu.name, this); 6072 menuBar.addItem(mc); 6073 mcs[menu.name] = mc; 6074 } 6075 6076 if(separator) 6077 mc.addSeparator(); 6078 mc.addItem(new MenuItem(action)); 6079 } 6080 } 6081 } 6082 } 6083 6084 this.menuBar = menuBar; 6085 6086 if(toolbarActions.length) { 6087 auto tb = new ToolBar(toolbarActions, this); 6088 } 6089 } 6090 6091 void delegate()[string] accelerators; 6092 6093 override void defaultEventHandler_keydown(KeyDownEvent event) { 6094 auto str = event.originalKeyEvent.toStr; 6095 if(auto acl = str in accelerators) 6096 (*acl)(); 6097 super.defaultEventHandler_keydown(event); 6098 } 6099 6100 override void defaultEventHandler_mouseover(MouseOverEvent event) { 6101 super.defaultEventHandler_mouseover(event); 6102 if(this.statusBar !is null && event.target.statusTip.length) 6103 this.statusBar.parts[0].content = event.target.statusTip; 6104 else if(this.statusBar !is null && this.statusTip.length) 6105 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 6106 } 6107 6108 override void addChild(Widget c, int position = int.max) { 6109 if(auto tb = cast(ToolBar) c) 6110 version(win32_widgets) 6111 super.addChild(c, 0); 6112 else version(custom_widgets) 6113 super.addChild(c, menuBar ? 1 : 0); 6114 else static assert(0); 6115 else 6116 clientArea.addChild(c, position); 6117 } 6118 6119 ToolBar _toolBar; 6120 /// 6121 ToolBar toolBar() { return _toolBar; } 6122 /// 6123 ToolBar toolBar(ToolBar t) { 6124 _toolBar = t; 6125 foreach(child; this.children) 6126 if(child is t) 6127 return t; 6128 version(win32_widgets) 6129 super.addChild(t, 0); 6130 else version(custom_widgets) 6131 super.addChild(t, menuBar ? 1 : 0); 6132 else static assert(0); 6133 return t; 6134 } 6135 6136 MenuBar _menu; 6137 /// 6138 MenuBar menuBar() { return _menu; } 6139 /// 6140 MenuBar menuBar(MenuBar m) { 6141 if(m is _menu) { 6142 version(custom_widgets) 6143 recomputeChildLayout(); 6144 return m; 6145 } 6146 6147 if(_menu !is null) { 6148 // make sure it is sanely removed 6149 // FIXME 6150 } 6151 6152 _menu = m; 6153 6154 version(win32_widgets) { 6155 SetMenu(parentWindow.win.impl.hwnd, m.handle); 6156 } else version(custom_widgets) { 6157 super.addChild(m, 0); 6158 6159 // clientArea.y = menu.height; 6160 // clientArea.height = this.height - menu.height; 6161 6162 recomputeChildLayout(); 6163 } else static assert(false); 6164 6165 return _menu; 6166 } 6167 private Widget _clientArea; 6168 /// 6169 @property Widget clientArea() { return _clientArea; } 6170 protected @property void clientArea(Widget wid) { 6171 _clientArea = wid; 6172 } 6173 6174 private StatusBar _statusBar; 6175 /// 6176 @property StatusBar statusBar() { return _statusBar; } 6177 /// 6178 @property void statusBar(StatusBar bar) { 6179 _statusBar = bar; 6180 super.addChild(_statusBar); 6181 } 6182 6183 /// 6184 @property string title() { return parentWindow.win.title; } 6185 /// 6186 @property void title(string title) { parentWindow.win.title = title; } 6187 } 6188 6189 class ClientAreaWidget : Widget { 6190 this(Widget parent = null) { 6191 super(parent); 6192 //sa = new ScrollableWidget(this); 6193 } 6194 /* 6195 ScrollableWidget sa; 6196 override void addChild(Widget w, int position) { 6197 if(sa is null) 6198 super.addChild(w, position); 6199 else { 6200 sa.addChild(w, position); 6201 sa.setContentSize(this.minWidth + 1, this.minHeight); 6202 import std.stdio; writeln(sa.contentWidth, "x", sa.contentHeight); 6203 } 6204 } 6205 */ 6206 } 6207 6208 /** 6209 Toolbars are lists of buttons (typically icons) that appear under the menu. 6210 Each button ought to correspond to a menu item. 6211 */ 6212 class ToolBar : Widget { 6213 version(win32_widgets) { 6214 private const int idealHeight; 6215 override int minHeight() { return idealHeight; } 6216 override int maxHeight() { return idealHeight; } 6217 } else version(custom_widgets) { 6218 override int minHeight() { return toolbarIconSize; }// Window.lineHeight * 3/2; } 6219 override int maxHeight() { return toolbarIconSize; } //Window.lineHeight * 3/2; } 6220 } else static assert(false); 6221 override int heightStretchiness() { return 0; } 6222 6223 version(win32_widgets) 6224 HIMAGELIST imageList; 6225 6226 this(Widget parent) { 6227 this(null, parent); 6228 } 6229 6230 /// 6231 this(Action[] actions, Widget parent = null) { 6232 super(parent); 6233 6234 tabStop = false; 6235 6236 version(win32_widgets) { 6237 // so i like how the flat thing looks on windows, but not on wine 6238 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 6239 // leave it commented 6240 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 6241 6242 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 6243 6244 imageList = ImageList_Create( 6245 // width, height 6246 16, 16, 6247 ILC_COLOR16 | ILC_MASK, 6248 16 /*numberOfButtons*/, 0); 6249 6250 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageList); 6251 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 6252 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 6253 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 6254 6255 TBBUTTON[] buttons; 6256 6257 // FIXME: I_IMAGENONE is if here is no icon 6258 foreach(action; actions) 6259 buttons ~= TBBUTTON( 6260 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 6261 action.id, 6262 TBSTATE_ENABLED, // state 6263 0, // style 6264 0, // reserved array, just zero it out 6265 0, // dwData 6266 cast(size_t) toWstringzInternal(action.label) // INT_PTR 6267 ); 6268 6269 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 6270 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 6271 6272 SIZE size; 6273 import core.sys.windows.commctrl; 6274 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 6275 idealHeight = size.cy + 4; // the plus 4 is a hack 6276 6277 /* 6278 RECT rect; 6279 GetWindowRect(hwnd, &rect); 6280 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 6281 */ 6282 6283 assert(idealHeight); 6284 } else version(custom_widgets) { 6285 foreach(action; actions) 6286 addChild(new ToolButton(action)); 6287 } else static assert(false); 6288 } 6289 6290 override void recomputeChildLayout() { 6291 .recomputeChildLayout!"width"(this); 6292 } 6293 } 6294 6295 enum toolbarIconSize = 24; 6296 6297 /// 6298 class ToolButton : Button { 6299 /// 6300 this(string label, Widget parent = null) { 6301 super(label, parent); 6302 tabStop = false; 6303 } 6304 /// 6305 this(Action action, Widget parent = null) { 6306 super(action.label, parent); 6307 tabStop = false; 6308 this.action = action; 6309 } 6310 6311 version(custom_widgets) 6312 override void defaultEventHandler_click(ClickEvent event) { 6313 foreach(handler; action.triggered) 6314 handler(); 6315 } 6316 6317 Action action; 6318 6319 override int maxWidth() { return toolbarIconSize; } 6320 override int minWidth() { return toolbarIconSize; } 6321 override int maxHeight() { return toolbarIconSize; } 6322 override int minHeight() { return toolbarIconSize; } 6323 6324 version(custom_widgets) 6325 override void paint(WidgetPainter painter) { 6326 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 6327 painter.outlineColor = Color.black; 6328 6329 // I want to get from 16 to 24. that's * 3 / 2 6330 static assert(toolbarIconSize >= 16); 6331 enum multiplier = toolbarIconSize / 8; 6332 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 6333 switch(action.iconId) { 6334 case GenericIcons.New: 6335 painter.fillColor = Color.white; 6336 painter.drawPolygon( 6337 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 6338 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 6339 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 6340 ); 6341 break; 6342 case GenericIcons.Save: 6343 painter.fillColor = Color.white; 6344 painter.outlineColor = Color.black; 6345 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 6346 6347 // the label 6348 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 6349 6350 // the slider 6351 painter.fillColor = Color.black; 6352 painter.outlineColor = Color.black; 6353 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 6354 6355 painter.fillColor = Color.white; 6356 painter.outlineColor = Color.white; 6357 // the disc window 6358 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 6359 break; 6360 case GenericIcons.Open: 6361 painter.fillColor = Color.white; 6362 painter.drawPolygon( 6363 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 6364 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 6365 painter.drawPolygon( 6366 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 6367 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 6368 Point(2, 6) * multiplier / divisor); 6369 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 6370 break; 6371 case GenericIcons.Copy: 6372 painter.fillColor = Color.white; 6373 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 6374 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 6375 break; 6376 case GenericIcons.Cut: 6377 painter.fillColor = Color.transparent; 6378 painter.outlineColor = getComputedStyle.foregroundColor(); 6379 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 6380 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 6381 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 6382 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 6383 break; 6384 case GenericIcons.Paste: 6385 painter.fillColor = Color.white; 6386 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 6387 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 6388 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 6389 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 6390 painter.fillColor = Color.black; 6391 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 6392 break; 6393 case GenericIcons.Help: 6394 painter.outlineColor = getComputedStyle.foregroundColor(); 6395 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 6396 break; 6397 case GenericIcons.Undo: 6398 painter.fillColor = Color.transparent; 6399 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 6400 painter.outlineColor = Color.black; 6401 painter.fillColor = Color.black; 6402 painter.drawPolygon( 6403 Point(4, 4) * multiplier / divisor, 6404 Point(8, 2) * multiplier / divisor, 6405 Point(8, 6) * multiplier / divisor, 6406 Point(4, 4) * multiplier / divisor, 6407 ); 6408 break; 6409 case GenericIcons.Redo: 6410 painter.fillColor = Color.transparent; 6411 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 6412 painter.outlineColor = Color.black; 6413 painter.fillColor = Color.black; 6414 painter.drawPolygon( 6415 Point(10, 4) * multiplier / divisor, 6416 Point(6, 2) * multiplier / divisor, 6417 Point(6, 6) * multiplier / divisor, 6418 Point(10, 4) * multiplier / divisor, 6419 ); 6420 break; 6421 default: 6422 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 6423 } 6424 return bounds; 6425 }); 6426 } 6427 6428 } 6429 6430 6431 /// 6432 class MenuBar : Widget { 6433 MenuItem[] items; 6434 Menu[] subMenus; 6435 6436 version(win32_widgets) { 6437 HMENU handle; 6438 /// 6439 this(Widget parent = null) { 6440 super(parent); 6441 6442 handle = CreateMenu(); 6443 tabStop = false; 6444 } 6445 } else version(custom_widgets) { 6446 /// 6447 this(Widget parent = null) { 6448 tabStop = false; // these are selected some other way 6449 super(parent); 6450 } 6451 6452 mixin Padding!q{2}; 6453 } else static assert(false); 6454 6455 version(custom_widgets) 6456 override void paint(WidgetPainter painter) { 6457 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().backgroundColor); 6458 } 6459 6460 /// 6461 MenuItem addItem(MenuItem item) { 6462 this.addChild(item); 6463 items ~= item; 6464 version(win32_widgets) { 6465 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 6466 } 6467 return item; 6468 } 6469 6470 6471 /// 6472 Menu addItem(Menu item) { 6473 6474 subMenus ~= item; 6475 6476 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 6477 6478 addChild(mbItem); 6479 items ~= mbItem; 6480 6481 version(win32_widgets) { 6482 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 6483 } else version(custom_widgets) { 6484 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 6485 item.popup(mbItem); 6486 }; 6487 } else static assert(false); 6488 6489 return item; 6490 } 6491 6492 override void recomputeChildLayout() { 6493 .recomputeChildLayout!"width"(this); 6494 } 6495 6496 override int maxHeight() { return Window.lineHeight + 4; } 6497 override int minHeight() { return Window.lineHeight + 4; } 6498 } 6499 6500 6501 /** 6502 Status bars appear at the bottom of a MainWindow. 6503 They are made out of Parts, with a width and content. 6504 6505 They can have multiple parts or be in simple mode. FIXME: implement 6506 6507 6508 sb.parts[0].content = "Status bar text!"; 6509 */ 6510 class StatusBar : Widget { 6511 private Part[] partsArray; 6512 /// 6513 struct Parts { 6514 @disable this(); 6515 this(StatusBar owner) { this.owner = owner; } 6516 //@disable this(this); 6517 /// 6518 @property int length() { return cast(int) owner.partsArray.length; } 6519 private StatusBar owner; 6520 private this(StatusBar owner, Part[] parts) { 6521 this.owner.partsArray = parts; 6522 this.owner = owner; 6523 } 6524 /// 6525 Part opIndex(int p) { 6526 if(owner.partsArray.length == 0) 6527 this ~= new StatusBar.Part(300); 6528 return owner.partsArray[p]; 6529 } 6530 6531 /// 6532 Part opOpAssign(string op : "~" )(Part p) { 6533 assert(owner.partsArray.length < 255); 6534 p.owner = this.owner; 6535 p.idx = cast(int) owner.partsArray.length; 6536 owner.partsArray ~= p; 6537 version(win32_widgets) { 6538 int[256] pos; 6539 int cpos = 0; 6540 foreach(idx, part; owner.partsArray) { 6541 if(part.width) 6542 cpos += part.width; 6543 else 6544 cpos += 100; 6545 6546 if(idx + 1 == owner.partsArray.length) 6547 pos[idx] = -1; 6548 else 6549 pos[idx] = cpos; 6550 } 6551 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 6552 } else version(custom_widgets) { 6553 owner.redraw(); 6554 } else static assert(false); 6555 6556 return p; 6557 } 6558 } 6559 6560 private Parts _parts; 6561 /// 6562 @property Parts parts() { 6563 return _parts; 6564 } 6565 6566 /// 6567 static class Part { 6568 int width; 6569 StatusBar owner; 6570 6571 /// 6572 this(int w = 100) { width = w; } 6573 6574 private int idx; 6575 private string _content; 6576 /// 6577 @property string content() { return _content; } 6578 /// 6579 @property void content(string s) { 6580 version(win32_widgets) { 6581 _content = s; 6582 WCharzBuffer bfr = WCharzBuffer(s); 6583 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 6584 } else version(custom_widgets) { 6585 if(_content != s) { 6586 _content = s; 6587 owner.redraw(); 6588 } 6589 } else static assert(false); 6590 } 6591 } 6592 string simpleModeContent; 6593 bool inSimpleMode; 6594 6595 6596 /// 6597 this(Widget parent = null) { 6598 super(null); // FIXME 6599 _parts = Parts(this); 6600 tabStop = false; 6601 version(win32_widgets) { 6602 parentWindow = parent.parentWindow; 6603 createWin32Window(this, "msctls_statusbar32"w, "", 0); 6604 6605 RECT rect; 6606 GetWindowRect(hwnd, &rect); 6607 idealHeight = rect.bottom - rect.top; 6608 assert(idealHeight); 6609 } else version(custom_widgets) { 6610 } else static assert(false); 6611 } 6612 6613 version(custom_widgets) 6614 override void paint(WidgetPainter painter) { 6615 auto cs = getComputedStyle(); 6616 this.draw3dFrame(painter, FrameStyle.sunk, cs.backgroundColor); 6617 int cpos = 0; 6618 int remainingLength = this.width; 6619 foreach(idx, part; this.partsArray) { 6620 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 6621 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 6622 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.backgroundColor); 6623 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 6624 6625 painter.outlineColor = cs.foregroundColor(); 6626 painter.fillColor = cs.foregroundColor(); 6627 6628 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 6629 cpos += partWidth; 6630 remainingLength -= partWidth; 6631 } 6632 } 6633 6634 6635 version(win32_widgets) { 6636 private const int idealHeight; 6637 override int maxHeight() { return idealHeight; } 6638 override int minHeight() { return idealHeight; } 6639 } else version(custom_widgets) { 6640 override int maxHeight() { return Window.lineHeight + 4; } 6641 override int minHeight() { return Window.lineHeight + 4; } 6642 } else static assert(false); 6643 } 6644 6645 /// Displays an in-progress indicator without known values 6646 version(none) 6647 class IndefiniteProgressBar : Widget { 6648 version(win32_widgets) 6649 this(Widget parent = null) { 6650 super(parent); 6651 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 6652 tabStop = false; 6653 } 6654 override int minHeight() { return 10; } 6655 } 6656 6657 /// A progress bar with a known endpoint and completion amount 6658 class ProgressBar : Widget { 6659 this(Widget parent = null) { 6660 version(win32_widgets) { 6661 super(parent); 6662 createWin32Window(this, "msctls_progress32"w, "", 0); 6663 tabStop = false; 6664 } else version(custom_widgets) { 6665 super(parent); 6666 max = 100; 6667 step = 10; 6668 tabStop = false; 6669 } else static assert(0); 6670 } 6671 6672 version(custom_widgets) 6673 override void paint(WidgetPainter painter) { 6674 auto cs = getComputedStyle(); 6675 this.draw3dFrame(painter, FrameStyle.sunk, cs.backgroundColor); 6676 painter.fillColor = cs.progressBarColor; 6677 painter.drawRectangle(Point(0, 0), width * current / max, height); 6678 } 6679 6680 6681 version(custom_widgets) { 6682 int current; 6683 int max; 6684 int step; 6685 } 6686 6687 /// 6688 void advanceOneStep() { 6689 version(win32_widgets) 6690 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 6691 else version(custom_widgets) 6692 addToPosition(step); 6693 else static assert(false); 6694 } 6695 6696 /// 6697 void setStepIncrement(int increment) { 6698 version(win32_widgets) 6699 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 6700 else version(custom_widgets) 6701 step = increment; 6702 else static assert(false); 6703 } 6704 6705 /// 6706 void addToPosition(int amount) { 6707 version(win32_widgets) 6708 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 6709 else version(custom_widgets) 6710 setPosition(current + amount); 6711 else static assert(false); 6712 } 6713 6714 /// 6715 void setPosition(int pos) { 6716 version(win32_widgets) 6717 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 6718 else version(custom_widgets) { 6719 current = pos; 6720 if(current > max) 6721 current = max; 6722 redraw(); 6723 } 6724 else static assert(false); 6725 } 6726 6727 /// 6728 void setRange(ushort min, ushort max) { 6729 version(win32_widgets) 6730 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 6731 else version(custom_widgets) { 6732 this.max = max; 6733 } 6734 else static assert(false); 6735 } 6736 6737 override int minHeight() { return 10; } 6738 } 6739 6740 /// 6741 class Fieldset : Widget { 6742 // FIXME: on Windows,it doesn't draw the background on the label 6743 // on X, it doesn't fix the clipping rectangle for it 6744 version(win32_widgets) 6745 override int paddingTop() { return Window.lineHeight; } 6746 else version(custom_widgets) 6747 override int paddingTop() { return Window.lineHeight + 2; } 6748 else static assert(false); 6749 override int paddingBottom() { return 6; } 6750 override int paddingLeft() { return 6; } 6751 override int paddingRight() { return 6; } 6752 6753 override int marginLeft() { return 6; } 6754 override int marginRight() { return 6; } 6755 override int marginTop() { return 2; } 6756 override int marginBottom() { return 2; } 6757 6758 string legend; 6759 6760 /// 6761 this(string legend, Widget parent) { 6762 version(win32_widgets) { 6763 super(parent); 6764 this.legend = legend; 6765 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 6766 tabStop = false; 6767 } else version(custom_widgets) { 6768 super(parent); 6769 tabStop = false; 6770 this.legend = legend; 6771 } else static assert(0); 6772 } 6773 6774 version(custom_widgets) 6775 override void paint(WidgetPainter painter) { 6776 painter.fillColor = Color.transparent; 6777 auto cs = getComputedStyle(); 6778 painter.pen = Pen(cs.foregroundColor, 1); 6779 painter.drawRectangle(Point(0, Window.lineHeight / 2), width, height - Window.lineHeight / 2); 6780 6781 auto tx = painter.textSize(legend); 6782 painter.outlineColor = Color.transparent; 6783 6784 static if(UsingSimpledisplayX11) { 6785 painter.fillColor = getComputedStyle().windowBackgroundColor; 6786 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 6787 } else version(Windows) { 6788 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 6789 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 6790 SelectObject(painter.impl.hdc, b); 6791 } else static assert(0); 6792 painter.outlineColor = cs.foregroundColor; 6793 painter.drawText(Point(8, 0), legend); 6794 } 6795 6796 6797 override int maxHeight() { 6798 auto m = paddingTop() + paddingBottom(); 6799 foreach(child; children) { 6800 auto mh = child.maxHeight(); 6801 if(mh == int.max) 6802 return int.max; 6803 m += mh; 6804 m += child.marginBottom(); 6805 m += child.marginTop(); 6806 } 6807 m += 6; 6808 if(m < minHeight) 6809 return minHeight; 6810 return m; 6811 } 6812 6813 override int minHeight() { 6814 auto m = paddingTop() + paddingBottom(); 6815 foreach(child; children) { 6816 m += child.minHeight(); 6817 m += child.marginBottom(); 6818 m += child.marginTop(); 6819 } 6820 return m + 6; 6821 } 6822 } 6823 6824 /// Draws a line 6825 class HorizontalRule : Widget { 6826 mixin Margin!q{ 2 }; 6827 override int minHeight() { return 2; } 6828 override int maxHeight() { return 2; } 6829 6830 /// 6831 this(Widget parent = null) { 6832 super(parent); 6833 } 6834 6835 override void paint(WidgetPainter painter) { 6836 auto cs = getComputedStyle(); 6837 painter.outlineColor = cs.darkAccentColor; 6838 painter.drawLine(Point(0, 0), Point(width, 0)); 6839 painter.outlineColor = cs.lightAccentColor; 6840 painter.drawLine(Point(0, 1), Point(width, 1)); 6841 } 6842 } 6843 6844 /// ditto 6845 class VerticalRule : Widget { 6846 mixin Margin!q{ 2 }; 6847 override int minWidth() { return 2; } 6848 override int maxWidth() { return 2; } 6849 6850 /// 6851 this(Widget parent = null) { 6852 super(parent); 6853 } 6854 6855 override void paint(WidgetPainter painter) { 6856 auto cs = getComputedStyle(); 6857 painter.outlineColor = cs.darkAccentColor; 6858 painter.drawLine(Point(0, 0), Point(0, height)); 6859 painter.outlineColor = cs.lightAccentColor; 6860 painter.drawLine(Point(1, 0), Point(1, height)); 6861 } 6862 } 6863 6864 6865 /// 6866 class Menu : Window { 6867 void remove() { 6868 foreach(i, child; parentWindow.children) 6869 if(child is this) { 6870 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 6871 break; 6872 } 6873 parentWindow.redraw(); 6874 6875 parentWindow.releaseMouseCapture(); 6876 } 6877 6878 /// 6879 void addSeparator() { 6880 version(win32_widgets) 6881 AppendMenu(handle, MF_SEPARATOR, 0, null); 6882 else version(custom_widgets) 6883 auto hr = new HorizontalRule(this); 6884 else static assert(0); 6885 } 6886 6887 override int paddingTop() { return 4; } 6888 override int paddingBottom() { return 4; } 6889 override int paddingLeft() { return 2; } 6890 override int paddingRight() { return 2; } 6891 6892 version(win32_widgets) {} 6893 else version(custom_widgets) { 6894 SimpleWindow dropDown; 6895 Widget menuParent; 6896 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 6897 this.menuParent = parent; 6898 6899 int w = 150; 6900 int h = paddingTop + paddingBottom; 6901 if(this.children.length) { 6902 // hacking it to get the ideal height out of recomputeChildLayout 6903 this.width = w; 6904 this.height = h; 6905 this.recomputeChildLayout(); 6906 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 6907 h += paddingBottom; 6908 6909 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 6910 } 6911 6912 if(offsetY == int.min) 6913 offsetY = parent.parentWindow.lineHeight; 6914 6915 auto coord = parent.globalCoordinates(); 6916 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 6917 this.x = 0; 6918 this.y = 0; 6919 this.width = dropDown.width; 6920 this.height = dropDown.height; 6921 this.drawableWindow = dropDown; 6922 this.recomputeChildLayout(); 6923 6924 static if(UsingSimpledisplayX11) 6925 XSync(XDisplayConnection.get, 0); 6926 6927 dropDown.visibilityChanged = (bool visible) { 6928 if(visible) { 6929 this.redraw(); 6930 dropDown.grabInput(); 6931 } else { 6932 dropDown.releaseInputGrab(); 6933 } 6934 }; 6935 6936 dropDown.show(); 6937 6938 bool firstClick = true; 6939 6940 clickListener = this.addEventListener(EventType.click, (Event ev) { 6941 if(firstClick) { 6942 firstClick = false; 6943 //return; 6944 } 6945 //if(ev.clientX < 0 || ev.clientY < 0 || ev.clientX > width || ev.clientY > height) 6946 unpopup(); 6947 }); 6948 } 6949 6950 EventListener clickListener; 6951 } 6952 else static assert(false); 6953 6954 version(custom_widgets) 6955 void unpopup() { 6956 mouseLastOver = mouseLastDownOn = null; 6957 dropDown.hide(); 6958 if(!menuParent.parentWindow.win.closed) { 6959 if(auto maw = cast(MouseActivatedWidget) menuParent) { 6960 maw.setDynamicState(DynamicState.depressed, false); 6961 maw.redraw(); 6962 } 6963 menuParent.parentWindow.win.focus(); 6964 } 6965 clickListener.disconnect(); 6966 } 6967 6968 MenuItem[] items; 6969 6970 /// 6971 MenuItem addItem(MenuItem item) { 6972 addChild(item); 6973 items ~= item; 6974 version(win32_widgets) { 6975 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 6976 } 6977 return item; 6978 } 6979 6980 string label; 6981 6982 version(win32_widgets) { 6983 HMENU handle; 6984 /// 6985 this(string label, Widget parent = null) { 6986 // not actually passing the parent since it effs up the drawing 6987 super(cast(Widget) null);// parent); 6988 this.label = label; 6989 handle = CreatePopupMenu(); 6990 } 6991 } else version(custom_widgets) { 6992 /// 6993 this(string label, Widget parent = null) { 6994 6995 if(dropDown) { 6996 dropDown.close(); 6997 } 6998 dropDown = new SimpleWindow( 6999 150, 4, 7000 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 7001 7002 this.label = label; 7003 7004 super(dropDown); 7005 } 7006 } else static assert(false); 7007 7008 override int maxHeight() { return Window.lineHeight; } 7009 override int minHeight() { return Window.lineHeight; } 7010 7011 version(custom_widgets) 7012 override void paint(WidgetPainter painter) { 7013 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.backgroundColor); 7014 } 7015 } 7016 7017 /// 7018 class MenuItem : MouseActivatedWidget { 7019 Menu submenu; 7020 7021 Action action; 7022 string label; 7023 7024 override int paddingLeft() { return 4; } 7025 7026 override int maxHeight() { return Window.lineHeight + 4; } 7027 override int minHeight() { return Window.lineHeight + 4; } 7028 override int minWidth() { return Window.lineHeight * cast(int) label.length + 8; } 7029 override int maxWidth() { 7030 if(cast(MenuBar) parent) { 7031 return Window.lineHeight / 2 * cast(int) label.length + 8; 7032 } 7033 return int.max; 7034 } 7035 /// 7036 this(string lbl, Widget parent = null) { 7037 super(parent); 7038 //label = lbl; // FIXME 7039 foreach(char ch; lbl) // FIXME 7040 if(ch != '&') // FIXME 7041 label ~= ch; // FIXME 7042 tabStop = false; // these are selected some other way 7043 } 7044 7045 version(custom_widgets) 7046 override void paint(WidgetPainter painter) { 7047 auto cs = getComputedStyle(); 7048 if(dynamicState & DynamicState.depressed) 7049 this.draw3dFrame(painter, FrameStyle.sunk, cs.backgroundColor); 7050 if(dynamicState & DynamicState.hover) 7051 painter.outlineColor = cs.activeMenuItemColor; 7052 else 7053 painter.outlineColor = cs.foregroundColor; 7054 painter.fillColor = Color.transparent; 7055 painter.drawText(Point(cast(MenuBar) this.parent ? 4 : 20, 2), label, Point(width, height), TextAlignment.Left); 7056 if(action && action.accelerator !is KeyEvent.init) { 7057 painter.drawText(Point(cast(MenuBar) this.parent ? 4 : 20, 2), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right); 7058 7059 } 7060 } 7061 7062 7063 /// 7064 this(Action action, Widget parent = null) { 7065 assert(action !is null); 7066 this(action.label); 7067 this.action = action; 7068 tabStop = false; // these are selected some other way 7069 } 7070 7071 override void defaultEventHandler_triggered(Event event) { 7072 if(action) 7073 foreach(handler; action.triggered) 7074 handler(); 7075 7076 if(auto pmenu = cast(Menu) this.parent) 7077 pmenu.remove(); 7078 7079 super.defaultEventHandler_triggered(event); 7080 } 7081 } 7082 7083 version(win32_widgets) 7084 /// 7085 class MouseActivatedWidget : Widget { 7086 bool isChecked() { 7087 assert(hwnd); 7088 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 7089 7090 } 7091 void isChecked(bool state) { 7092 assert(hwnd); 7093 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 7094 7095 } 7096 7097 override void handleWmCommand(ushort cmd, ushort id) { 7098 if(cmd == 0) { 7099 auto event = new Event(EventType.triggered, this); 7100 event.dispatch(); 7101 } 7102 } 7103 7104 this(Widget parent = null) { 7105 super(parent); 7106 } 7107 } 7108 else version(custom_widgets) 7109 /// 7110 class MouseActivatedWidget : Widget { 7111 @property bool isChecked() { return isChecked_; } 7112 @property bool isChecked(bool b) { return isChecked_ = b; } 7113 7114 private bool isChecked_; 7115 7116 override void attachedToWindow(Window w) { 7117 w.addEventListener("mouseup", delegate (Widget _this, Event ev) { 7118 setDynamicState(DynamicState.depressed, false); 7119 }); 7120 } 7121 7122 this(Widget parent = null) { 7123 super(parent); 7124 7125 addEventListener("mousedown", delegate (Widget _this, Event ev) { 7126 setDynamicState(DynamicState.depressed, true); 7127 redraw(); 7128 }); 7129 7130 addEventListener("mouseup", delegate (Widget _this, Event ev) { 7131 setDynamicState(DynamicState.depressed, false); 7132 redraw(); 7133 }); 7134 } 7135 7136 override void defaultEventHandler_focus(Event ev) { 7137 super.defaultEventHandler_focus(ev); 7138 this.redraw(); 7139 } 7140 override void defaultEventHandler_blur(Event ev) { 7141 super.defaultEventHandler_blur(ev); 7142 setDynamicState(DynamicState.depressed, false); 7143 this.redraw(); 7144 } 7145 override void defaultEventHandler_keydown(KeyDownEvent ev) { 7146 super.defaultEventHandler_keydown(ev); 7147 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 7148 setDynamicState(DynamicState.depressed, true); 7149 this.redraw(); 7150 } 7151 } 7152 override void defaultEventHandler_keyup(KeyUpEvent ev) { 7153 super.defaultEventHandler_keyup(ev); 7154 if(!(dynamicState & DynamicState.depressed)) 7155 return; 7156 setDynamicState(DynamicState.depressed, false); 7157 this.redraw(); 7158 7159 auto event = new Event(EventType.triggered, this); 7160 event.sendDirectly(); 7161 } 7162 override void defaultEventHandler_click(ClickEvent ev) { 7163 super.defaultEventHandler_click(ev); 7164 if(this.tabStop) 7165 this.focus(); 7166 auto event = new Event(EventType.triggered, this); 7167 event.sendDirectly(); 7168 } 7169 7170 } 7171 else static assert(false); 7172 7173 /* 7174 /++ 7175 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 7176 7177 Basically the same as a checkbox. 7178 +/ 7179 class OnOffSwitch : MouseActivatedWidget { 7180 7181 } 7182 */ 7183 7184 /// 7185 class Checkbox : MouseActivatedWidget { 7186 7187 version(win32_widgets) { 7188 override int maxHeight() { return 16; } 7189 override int minHeight() { return 16; } 7190 } else version(custom_widgets) { 7191 override int maxHeight() { return Window.lineHeight; } 7192 override int minHeight() { return Window.lineHeight; } 7193 } else static assert(0); 7194 7195 override int marginLeft() { return 4; } 7196 7197 private string label; 7198 7199 /// 7200 this(string label, Widget parent = null) { 7201 super(parent); 7202 this.label = label; 7203 version(win32_widgets) { 7204 createWin32Window(this, "button"w, label, BS_CHECKBOX); 7205 } else version(custom_widgets) { 7206 7207 } else static assert(0); 7208 } 7209 7210 version(custom_widgets) 7211 override void paint(WidgetPainter painter) { 7212 auto cs = getComputedStyle(); 7213 if(isFocused()) { 7214 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 7215 painter.fillColor = cs.windowBackgroundColor; 7216 painter.drawRectangle(Point(0, 0), width, height); 7217 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 7218 } else { 7219 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 7220 painter.fillColor = cs.windowBackgroundColor; 7221 painter.drawRectangle(Point(0, 0), width, height); 7222 } 7223 7224 7225 enum buttonSize = 16; 7226 7227 painter.outlineColor = Color.black; 7228 painter.fillColor = Color.white; 7229 painter.drawRectangle(Point(2, 2), buttonSize - 2, buttonSize - 2); 7230 7231 if(isChecked) { 7232 painter.pen = Pen(Color.black, 2); 7233 // I'm using height so the checkbox is square 7234 enum padding = 5; 7235 painter.drawLine(Point(padding, padding), Point(buttonSize - (padding-2), buttonSize - (padding-2))); 7236 painter.drawLine(Point(buttonSize-(padding-2), padding), Point(padding, buttonSize - (padding-2))); 7237 7238 painter.pen = Pen(Color.black, 1); 7239 } 7240 7241 painter.outlineColor = cs.foregroundColor(); 7242 painter.fillColor = cs.foregroundColor(); 7243 7244 painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 7245 } 7246 7247 override void defaultEventHandler_triggered(Event ev) { 7248 isChecked = !isChecked; 7249 7250 this.emit!(ChangeEvent!bool)(&isChecked); 7251 7252 redraw(); 7253 } 7254 7255 /// Emits a change event with the checked state 7256 mixin Emits!(ChangeEvent!bool); 7257 } 7258 7259 /// Adds empty space to a layout. 7260 class VerticalSpacer : Widget { 7261 /// 7262 this(Widget parent = null) { 7263 super(parent); 7264 } 7265 } 7266 7267 /// ditto 7268 class HorizontalSpacer : Widget { 7269 /// 7270 this(Widget parent = null) { 7271 super(parent); 7272 this.tabStop = false; 7273 } 7274 } 7275 7276 7277 /// 7278 class Radiobox : MouseActivatedWidget { 7279 7280 version(win32_widgets) { 7281 override int maxHeight() { return 16; } 7282 override int minHeight() { return 16; } 7283 } else version(custom_widgets) { 7284 override int maxHeight() { return Window.lineHeight; } 7285 override int minHeight() { return Window.lineHeight; } 7286 } else static assert(0); 7287 7288 override int marginLeft() { return 4; } 7289 7290 private string label; 7291 7292 version(win32_widgets) 7293 this(string label, Widget parent = null) { 7294 super(parent); 7295 this.label = label; 7296 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 7297 } 7298 else version(custom_widgets) 7299 this(string label, Widget parent = null) { 7300 super(parent); 7301 this.label = label; 7302 height = 16; 7303 width = height + 4 + cast(int) label.length * 16; 7304 } 7305 else static assert(false); 7306 7307 version(custom_widgets) 7308 override void paint(WidgetPainter painter) { 7309 auto cs = getComputedStyle(); 7310 if(isFocused) { 7311 painter.fillColor = cs.windowBackgroundColor; 7312 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 7313 } else { 7314 painter.fillColor = cs.windowBackgroundColor; 7315 painter.outlineColor = cs.windowBackgroundColor; 7316 } 7317 painter.drawRectangle(Point(0, 0), width, height); 7318 7319 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 7320 7321 enum buttonSize = 16; 7322 7323 painter.outlineColor = Color.black; 7324 painter.fillColor = Color.white; 7325 painter.drawEllipse(Point(2, 2), Point(buttonSize - 2, buttonSize - 2)); 7326 if(isChecked) { 7327 painter.outlineColor = Color.black; 7328 painter.fillColor = Color.black; 7329 // I'm using height so the checkbox is square 7330 painter.drawEllipse(Point(5, 5), Point(buttonSize - 5, buttonSize - 5)); 7331 } 7332 7333 painter.outlineColor = cs.foregroundColor(); 7334 painter.fillColor = cs.foregroundColor(); 7335 7336 painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 7337 } 7338 7339 7340 override void defaultEventHandler_triggered(Event ev) { 7341 isChecked = true; 7342 7343 if(this.parent) { 7344 foreach(child; this.parent.children) { 7345 if(child is this) continue; 7346 if(auto rb = cast(Radiobox) child) { 7347 rb.isChecked = false; 7348 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 7349 rb.redraw(); 7350 } 7351 } 7352 } 7353 7354 this.emit!(ChangeEvent!bool)(&this.isChecked); 7355 7356 redraw(); 7357 } 7358 7359 /// 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. 7360 mixin Emits!(ChangeEvent!bool); 7361 } 7362 7363 7364 /// 7365 class Button : MouseActivatedWidget { 7366 override int heightStretchiness() { return 3; } 7367 override int widthStretchiness() { return 3; } 7368 7369 private string label_; 7370 7371 /// 7372 string label() { return label_; } 7373 /// 7374 void label(string l) { 7375 label_ = l; 7376 version(win32_widgets) { 7377 WCharzBuffer bfr = WCharzBuffer(l); 7378 SetWindowTextW(hwnd, bfr.ptr); 7379 } else version(custom_widgets) { 7380 redraw(); 7381 } 7382 } 7383 7384 version(win32_widgets) 7385 this(string label, Widget parent = null) { 7386 // FIXME: use ideal button size instead 7387 width = 50; 7388 height = 30; 7389 super(parent); 7390 createWin32Window(this, "button"w, label, BS_PUSHBUTTON); 7391 7392 this.label = label; 7393 } 7394 else version(custom_widgets) 7395 this(string label, Widget parent = null) { 7396 width = 50; 7397 height = 30; 7398 super(parent); 7399 7400 this.label = label; 7401 } 7402 else static assert(false); 7403 7404 override int minHeight() { return Window.lineHeight + 4; } 7405 7406 class Style : Widget.Style { 7407 override Color backgroundColor() { 7408 auto cs = getComputedStyle(); // FIXME: this is potentially recursive 7409 7410 auto pressed = DynamicState.depressed | DynamicState.hover; 7411 if((dynamicState & pressed) == pressed) { 7412 return cs.depressedButtonColor(); 7413 } else if(dynamicState & DynamicState.hover) { 7414 return cs.hoveringColor(); 7415 } else { 7416 return cs.buttonColor(); 7417 } 7418 } 7419 7420 override FrameStyle borderStyle() { 7421 auto pressed = DynamicState.depressed | DynamicState.hover; 7422 if((dynamicState & pressed) == pressed) { 7423 return FrameStyle.sunk; 7424 } else { 7425 return FrameStyle.risen; 7426 } 7427 7428 } 7429 } 7430 mixin OverrideStyle!Style; 7431 7432 version(custom_widgets) 7433 override void paint(WidgetPainter painter) { 7434 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 7435 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, TextAlignment.Center | TextAlignment.VerticalCenter); 7436 return bounds; 7437 }); 7438 } 7439 7440 } 7441 7442 /++ 7443 A button with a consistent size, suitable for user commands like OK and Cancel. 7444 +/ 7445 class CommandButton : Button { 7446 this(string label, Widget parent = null) { 7447 super(label, parent); 7448 } 7449 7450 override int maxHeight() { 7451 return Window.lineHeight + 4; 7452 } 7453 7454 override int maxWidth() { 7455 return Window.lineHeight * 4; 7456 } 7457 7458 override int marginLeft() { return 12; } 7459 override int marginRight() { return 12; } 7460 override int marginTop() { return 12; } 7461 override int marginBottom() { return 12; } 7462 } 7463 7464 /// 7465 enum ArrowDirection { 7466 left, /// 7467 right, /// 7468 up, /// 7469 down /// 7470 } 7471 7472 /// 7473 version(custom_widgets) 7474 class ArrowButton : Button { 7475 /// 7476 this(ArrowDirection direction, Widget parent = null) { 7477 super("", parent); 7478 this.direction = direction; 7479 } 7480 7481 private ArrowDirection direction; 7482 7483 override int minHeight() { return 16; } 7484 override int maxHeight() { return 16; } 7485 override int minWidth() { return 16; } 7486 override int maxWidth() { return 16; } 7487 7488 override void paint(WidgetPainter painter) { 7489 super.paint(painter); 7490 7491 auto cs = getComputedStyle(); 7492 7493 painter.outlineColor = cs.foregroundColor; 7494 painter.fillColor = cs.foregroundColor; 7495 7496 auto offset = Point((this.width - 16) / 2, (this.height - 16) / 2); 7497 7498 final switch(direction) { 7499 case ArrowDirection.up: 7500 painter.drawPolygon( 7501 Point(2, 10) + offset, 7502 Point(7, 5) + offset, 7503 Point(12, 10) + offset, 7504 Point(2, 10) + offset 7505 ); 7506 break; 7507 case ArrowDirection.down: 7508 painter.drawPolygon( 7509 Point(2, 6) + offset, 7510 Point(7, 11) + offset, 7511 Point(12, 6) + offset, 7512 Point(2, 6) + offset 7513 ); 7514 break; 7515 case ArrowDirection.left: 7516 painter.drawPolygon( 7517 Point(10, 2) + offset, 7518 Point(5, 7) + offset, 7519 Point(10, 12) + offset, 7520 Point(10, 2) + offset 7521 ); 7522 break; 7523 case ArrowDirection.right: 7524 painter.drawPolygon( 7525 Point(6, 2) + offset, 7526 Point(11, 7) + offset, 7527 Point(6, 12) + offset, 7528 Point(6, 2) + offset 7529 ); 7530 break; 7531 } 7532 } 7533 } 7534 7535 private 7536 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 7537 int x, y; 7538 Widget par = c; 7539 while(par) { 7540 x += par.x; 7541 y += par.y; 7542 par = par.parent; 7543 } 7544 return [x, y]; 7545 } 7546 7547 version(win32_widgets) 7548 private 7549 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 7550 int x, y; 7551 Widget par = c; 7552 while(par) { 7553 x += par.x; 7554 y += par.y; 7555 par = par.parent; 7556 if(par !is null && par.useNativeDrawing()) 7557 break; 7558 } 7559 return [x, y]; 7560 } 7561 7562 /// 7563 class ImageBox : Widget { 7564 private MemoryImage image_; 7565 7566 /// 7567 public void setImage(MemoryImage image){ 7568 this.image_ = image; 7569 if(this.parentWindow && this.parentWindow.win) 7570 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); 7571 redraw(); 7572 } 7573 7574 /// How to fit the image in the box if they aren't an exact match in size? 7575 enum HowToFit { 7576 center, /// centers the image, cropping around all the edges as needed 7577 crop, /// always draws the image in the upper left, cropping the lower right if needed 7578 // stretch, /// not implemented 7579 } 7580 7581 private Sprite sprite; 7582 private HowToFit howToFit_; 7583 7584 private Color backgroundColor_; 7585 7586 /// 7587 this(MemoryImage image, HowToFit howToFit, Color backgroundColor = Color.transparent, Widget parent = null) { 7588 this.image_ = image; 7589 this.tabStop = false; 7590 this.howToFit_ = howToFit; 7591 this.backgroundColor_ = backgroundColor; 7592 super(parent); 7593 updateSprite(); 7594 } 7595 7596 private void updateSprite() { 7597 if(sprite is null && this.parentWindow && this.parentWindow.win) { 7598 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); 7599 } 7600 } 7601 7602 override void paint(WidgetPainter painter) { 7603 updateSprite(); 7604 if(backgroundColor_.a) { 7605 painter.fillColor = backgroundColor_; 7606 painter.drawRectangle(Point(0, 0), width, height); 7607 } 7608 if(howToFit_ == HowToFit.crop) 7609 sprite.drawAt(painter, Point(0, 0)); 7610 else if(howToFit_ == HowToFit.center) { 7611 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 7612 } 7613 } 7614 } 7615 7616 /// 7617 class TextLabel : Widget { 7618 override int maxHeight() { return Window.lineHeight; } 7619 override int minHeight() { return Window.lineHeight; } 7620 override int minWidth() { return 32; } 7621 7622 string label_; 7623 7624 /// 7625 @scriptable 7626 string label() { return label_; } 7627 7628 /// 7629 @scriptable 7630 void label(string l) { 7631 label_ = l; 7632 version(win32_widgets) { 7633 WCharzBuffer bfr = WCharzBuffer(l); 7634 SetWindowTextW(hwnd, bfr.ptr); 7635 } else version(custom_widgets) 7636 redraw(); 7637 } 7638 7639 /// 7640 this(string label, Widget parent = null) { 7641 this(label, TextAlignment.Right, parent); 7642 } 7643 7644 /// 7645 this(string label, TextAlignment alignment, Widget parent = null) { 7646 this.label_ = label; 7647 this.alignment = alignment; 7648 this.tabStop = false; 7649 super(parent); 7650 7651 version(win32_widgets) 7652 createWin32Window(this, "static"w, label, 0, alignment == TextAlignment.Right ? WS_EX_RIGHT : WS_EX_LEFT); 7653 } 7654 7655 TextAlignment alignment; 7656 7657 version(custom_widgets) 7658 override void paint(WidgetPainter painter) { 7659 painter.outlineColor = getComputedStyle().foregroundColor; 7660 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 7661 } 7662 7663 } 7664 7665 version(custom_widgets) 7666 private struct etc { 7667 mixin ExperimentalTextComponent; 7668 } 7669 7670 version(win32_widgets) 7671 alias EditableTextWidgetParent = Widget; /// 7672 else version(custom_widgets) 7673 alias EditableTextWidgetParent = ScrollableWidget; /// 7674 else static assert(0); 7675 7676 /// Contains the implementation of text editing 7677 abstract class EditableTextWidget : EditableTextWidgetParent { 7678 this(Widget parent = null) { 7679 super(parent); 7680 } 7681 7682 bool wordWrapEnabled_ = false; 7683 void wordWrapEnabled(bool enabled) { 7684 version(win32_widgets) { 7685 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 7686 } else version(custom_widgets) { 7687 wordWrapEnabled_ = enabled; // FIXME 7688 } else static assert(false); 7689 } 7690 7691 override int minWidth() { return 16; } 7692 override int minHeight() { return Window.lineHeight + 0; } // the +0 is to leave room for the padding 7693 override int widthStretchiness() { return 7; } 7694 7695 void selectAll() { 7696 version(win32_widgets) 7697 SendMessage(hwnd, EM_SETSEL, 0, -1); 7698 else version(custom_widgets) { 7699 textLayout.selectAll(); 7700 redraw(); 7701 } 7702 } 7703 7704 @property string content() { 7705 version(win32_widgets) { 7706 wchar[4096] bufferstack; 7707 wchar[] buffer; 7708 auto len = GetWindowTextLength(hwnd); 7709 if(len < bufferstack.length) 7710 buffer = bufferstack[0 .. len + 1]; 7711 else 7712 buffer = new wchar[](len + 1); 7713 7714 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 7715 if(l >= 0) 7716 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 7717 else 7718 return null; 7719 } else version(custom_widgets) { 7720 return textLayout.getPlainText(); 7721 } else static assert(false); 7722 } 7723 @property void content(string s) { 7724 version(win32_widgets) { 7725 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 7726 SetWindowTextW(hwnd, bfr.ptr); 7727 } else version(custom_widgets) { 7728 textLayout.clear(); 7729 textLayout.addText(s); 7730 7731 { 7732 // FIXME: it should be able to get this info easier 7733 auto painter = draw(); 7734 textLayout.redoLayout(painter); 7735 } 7736 auto cbb = textLayout.contentBoundingBox(); 7737 setContentSize(cbb.width, cbb.height); 7738 /* 7739 textLayout.addText(ForegroundColor.red, s); 7740 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 7741 textLayout.addText(" is the best!"); 7742 */ 7743 redraw(); 7744 } 7745 else static assert(false); 7746 } 7747 7748 void addText(string txt) { 7749 version(custom_widgets) { 7750 7751 textLayout.addText(txt); 7752 7753 { 7754 // FIXME: it should be able to get this info easier 7755 auto painter = draw(); 7756 textLayout.redoLayout(painter); 7757 } 7758 auto cbb = textLayout.contentBoundingBox(); 7759 setContentSize(cbb.width, cbb.height); 7760 7761 } else version(win32_widgets) { 7762 // get the current selection 7763 DWORD StartPos, EndPos; 7764 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 7765 7766 // move the caret to the end of the text 7767 int outLength = GetWindowTextLengthW(hwnd); 7768 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 7769 7770 // insert the text at the new caret position 7771 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 7772 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 7773 7774 // restore the previous selection 7775 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 7776 } else static assert(0); 7777 } 7778 7779 version(custom_widgets) 7780 override void paintFrameAndBackground(WidgetPainter painter) { 7781 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 7782 } 7783 7784 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 7785 else version(custom_widgets) { 7786 // FIXME 7787 7788 static if(SimpledisplayTimerAvailable) 7789 Timer caretTimer; 7790 etc.TextLayout textLayout; 7791 7792 void setupCustomTextEditing() { 7793 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 7794 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 7795 } 7796 7797 override void paint(WidgetPainter painter) { 7798 if(parentWindow.win.closed) return; 7799 7800 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 7801 7802 /* 7803 painter.outlineColor = Color.white; 7804 painter.fillColor = Color.white; 7805 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 7806 */ 7807 7808 painter.outlineColor = Color.black; 7809 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 7810 7811 textLayout.caretShowingOnScreen = false; 7812 7813 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 7814 } 7815 7816 static class Style : Widget.Style { 7817 override MouseCursor cursor() { 7818 return GenericCursor.Text; 7819 } 7820 } 7821 mixin OverrideStyle!Style; 7822 } 7823 else static assert(false); 7824 7825 7826 7827 version(custom_widgets) 7828 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 7829 super.defaultEventHandler_mousedown(ev); 7830 if(parentWindow.win.closed) return; 7831 if(ev.button == MouseButton.left) { 7832 if(textLayout.selectNone()) 7833 redraw(); 7834 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 7835 this.focus(); 7836 //this.parentWindow.win.grabInput(); 7837 } else if(ev.button == MouseButton.middle) { 7838 static if(UsingSimpledisplayX11) { 7839 getPrimarySelection(parentWindow.win, (txt) { 7840 textLayout.insert(txt); 7841 redraw(); 7842 7843 auto cbb = textLayout.contentBoundingBox(); 7844 setContentSize(cbb.width, cbb.height); 7845 }); 7846 } 7847 } 7848 } 7849 7850 version(custom_widgets) 7851 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 7852 //this.parentWindow.win.releaseInputGrab(); 7853 super.defaultEventHandler_mouseup(ev); 7854 } 7855 7856 version(custom_widgets) 7857 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 7858 super.defaultEventHandler_mousemove(ev); 7859 if(ev.state & ModifierState.leftButtonDown) { 7860 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 7861 redraw(); 7862 } 7863 } 7864 7865 version(custom_widgets) 7866 override void defaultEventHandler_focus(Event ev) { 7867 super.defaultEventHandler_focus(ev); 7868 if(parentWindow.win.closed) return; 7869 auto painter = this.draw(); 7870 textLayout.drawCaret(painter); 7871 7872 static if(SimpledisplayTimerAvailable) 7873 if(caretTimer) { 7874 caretTimer.destroy(); 7875 caretTimer = null; 7876 } 7877 7878 bool blinkingCaret = true; 7879 static if(UsingSimpledisplayX11) 7880 if(!Image.impl.xshmAvailable) 7881 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 7882 7883 if(blinkingCaret) 7884 static if(SimpledisplayTimerAvailable) 7885 caretTimer = new Timer(500, { 7886 if(parentWindow.win.closed) { 7887 caretTimer.destroy(); 7888 return; 7889 } 7890 if(isFocused()) { 7891 auto painter = this.draw(); 7892 textLayout.drawCaret(painter); 7893 } else if(textLayout.caretShowingOnScreen) { 7894 auto painter = this.draw(); 7895 textLayout.eraseCaret(painter); 7896 } 7897 }); 7898 } 7899 7900 override void defaultEventHandler_blur(Event ev) { 7901 super.defaultEventHandler_blur(ev); 7902 if(parentWindow.win.closed) return; 7903 version(custom_widgets) { 7904 auto painter = this.draw(); 7905 textLayout.eraseCaret(painter); 7906 static if(SimpledisplayTimerAvailable) 7907 if(caretTimer) { 7908 caretTimer.destroy(); 7909 caretTimer = null; 7910 } 7911 } 7912 7913 auto evt = new ChangeEvent!string(this, &this.content); 7914 evt.dispatch(); 7915 } 7916 7917 version(custom_widgets) 7918 override void defaultEventHandler_char(CharEvent ev) { 7919 super.defaultEventHandler_char(ev); 7920 textLayout.insert(ev.character); 7921 redraw(); 7922 7923 // FIXME: too inefficient 7924 auto cbb = textLayout.contentBoundingBox(); 7925 setContentSize(cbb.width, cbb.height); 7926 } 7927 version(custom_widgets) 7928 override void defaultEventHandler_keydown(KeyDownEvent ev) { 7929 //super.defaultEventHandler_keydown(ev); 7930 switch(ev.key) { 7931 case Key.Delete: 7932 textLayout.delete_(); 7933 redraw(); 7934 break; 7935 case Key.Left: 7936 textLayout.moveLeft(); 7937 redraw(); 7938 break; 7939 case Key.Right: 7940 textLayout.moveRight(); 7941 redraw(); 7942 break; 7943 case Key.Up: 7944 textLayout.moveUp(); 7945 redraw(); 7946 break; 7947 case Key.Down: 7948 textLayout.moveDown(); 7949 redraw(); 7950 break; 7951 case Key.Home: 7952 textLayout.moveHome(); 7953 redraw(); 7954 break; 7955 case Key.End: 7956 textLayout.moveEnd(); 7957 redraw(); 7958 break; 7959 case Key.PageUp: 7960 foreach(i; 0 .. 32) 7961 textLayout.moveUp(); 7962 redraw(); 7963 break; 7964 case Key.PageDown: 7965 foreach(i; 0 .. 32) 7966 textLayout.moveDown(); 7967 redraw(); 7968 break; 7969 7970 default: 7971 {} // intentionally blank, let "char" handle it 7972 } 7973 /* 7974 if(ev.key == Key.Backspace) { 7975 textLayout.backspace(); 7976 redraw(); 7977 } 7978 */ 7979 ensureVisibleInScroll(textLayout.caretBoundingBox()); 7980 } 7981 7982 7983 } 7984 7985 /// 7986 class LineEdit : EditableTextWidget { 7987 // FIXME: hack 7988 version(custom_widgets) { 7989 override bool showingVerticalScroll() { return false; } 7990 override bool showingHorizontalScroll() { return false; } 7991 } 7992 7993 /// 7994 this(Widget parent = null) { 7995 super(parent); 7996 version(win32_widgets) { 7997 createWin32Window(this, "edit"w, "", 7998 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 7999 } else version(custom_widgets) { 8000 setupCustomTextEditing(); 8001 addEventListener(delegate(CharEvent ev) { 8002 if(ev.character == '\n') 8003 ev.preventDefault(); 8004 }); 8005 } else static assert(false); 8006 } 8007 override int maxHeight() { return Window.lineHeight + 4; } 8008 override int minHeight() { return Window.lineHeight + 4; } 8009 8010 /+ 8011 @property void passwordMode(bool p) { 8012 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 8013 } 8014 +/ 8015 } 8016 8017 /++ 8018 A [LineEdit] that displays `*` in place of the actual characters. 8019 8020 Alas, Windows requires the window to be created differently to use this style, 8021 so it had to be a new class instead of a toggle on and off on an existing object. 8022 8023 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 8024 8025 History: 8026 Added January 24, 2021 8027 +/ 8028 class PasswordEdit : EditableTextWidget { 8029 version(custom_widgets) { 8030 override bool showingVerticalScroll() { return false; } 8031 override bool showingHorizontalScroll() { return false; } 8032 } 8033 8034 /// 8035 this(Widget parent = null) { 8036 super(parent); 8037 version(win32_widgets) { 8038 createWin32Window(this, "edit"w, "", 8039 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 8040 } else version(custom_widgets) { 8041 setupCustomTextEditing(); 8042 addEventListener(delegate(CharEvent ev) { 8043 if(ev.character == '\n') 8044 ev.preventDefault(); 8045 }); 8046 } else static assert(false); 8047 } 8048 override int maxHeight() { return Window.lineHeight + 4; } 8049 override int minHeight() { return Window.lineHeight + 4; } 8050 } 8051 8052 8053 /// 8054 class TextEdit : EditableTextWidget { 8055 /// 8056 this(Widget parent = null) { 8057 super(parent); 8058 version(win32_widgets) { 8059 createWin32Window(this, "edit"w, "", 8060 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 8061 } else version(custom_widgets) { 8062 setupCustomTextEditing(); 8063 } else static assert(false); 8064 } 8065 override int maxHeight() { return int.max; } 8066 override int heightStretchiness() { return 7; } 8067 } 8068 8069 8070 /++ 8071 8072 +/ 8073 version(none) 8074 class RichTextDisplay : Widget { 8075 @property void content(string c) {} 8076 void appendContent(string c) {} 8077 } 8078 8079 /// 8080 class MessageBox : Window { 8081 private string message; 8082 MessageBoxButton buttonPressed = MessageBoxButton.None; 8083 /// 8084 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 8085 super(300, 100); 8086 8087 assert(buttons.length); 8088 assert(buttons.length == buttonIds.length); 8089 8090 this.message = message; 8091 8092 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 8093 8094 int x = this.width / 2 - buttonsWidth / 2; 8095 8096 foreach(idx, buttonText; buttons) { 8097 auto button = new Button(buttonText, this); 8098 button.x = x; 8099 button.y = height - (button.height + 10); 8100 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 8101 this.buttonPressed = buttonIds[idx]; 8102 win.close(); 8103 }; })(idx)); 8104 8105 button.registerMovement(); 8106 x += button.width; 8107 x += 16; 8108 if(idx == 0) 8109 button.focus(); 8110 } 8111 8112 win.show(); 8113 redraw(); 8114 } 8115 8116 override void paint(WidgetPainter painter) { 8117 super.paint(painter); 8118 8119 auto cs = getComputedStyle(); 8120 8121 painter.outlineColor = cs.foregroundColor(); 8122 painter.fillColor = cs.foregroundColor(); 8123 8124 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 8125 } 8126 8127 // this one is all fixed position 8128 override void recomputeChildLayout() {} 8129 } 8130 8131 /// 8132 enum MessageBoxStyle { 8133 OK, /// 8134 OKCancel, /// 8135 RetryCancel, /// 8136 YesNo, /// 8137 YesNoCancel, /// 8138 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. 8139 } 8140 8141 /// 8142 enum MessageBoxIcon { 8143 None, /// 8144 Info, /// 8145 Warning, /// 8146 Error /// 8147 } 8148 8149 /// Identifies the button the user pressed on a message box. 8150 enum MessageBoxButton { 8151 None, /// The user closed the message box without clicking any of the buttons. 8152 OK, /// 8153 Cancel, /// 8154 Retry, /// 8155 Yes, /// 8156 No, /// 8157 Continue /// 8158 } 8159 8160 8161 /++ 8162 Displays a modal message box, blocking until the user dismisses it. 8163 8164 Returns: the button pressed. 8165 +/ 8166 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8167 version(win32_widgets) { 8168 WCharzBuffer t = WCharzBuffer(title); 8169 WCharzBuffer m = WCharzBuffer(message); 8170 UINT type; 8171 with(MessageBoxStyle) 8172 final switch(style) { 8173 case OK: type |= MB_OK; break; 8174 case OKCancel: type |= MB_OKCANCEL; break; 8175 case RetryCancel: type |= MB_RETRYCANCEL; break; 8176 case YesNo: type |= MB_YESNO; break; 8177 case YesNoCancel: type |= MB_YESNOCANCEL; break; 8178 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 8179 } 8180 with(MessageBoxIcon) 8181 final switch(icon) { 8182 case None: break; 8183 case Info: type |= MB_ICONINFORMATION; break; 8184 case Warning: type |= MB_ICONWARNING; break; 8185 case Error: type |= MB_ICONERROR; break; 8186 } 8187 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 8188 case IDOK: return MessageBoxButton.OK; 8189 case IDCANCEL: return MessageBoxButton.Cancel; 8190 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 8191 case IDYES: return MessageBoxButton.Yes; 8192 case IDNO: return MessageBoxButton.No; 8193 case IDCONTINUE: return MessageBoxButton.Continue; 8194 default: return MessageBoxButton.None; 8195 } 8196 } else { 8197 string[] buttons; 8198 MessageBoxButton[] buttonIds; 8199 with(MessageBoxStyle) 8200 final switch(style) { 8201 case OK: 8202 buttons = ["OK"]; 8203 buttonIds = [MessageBoxButton.OK]; 8204 break; 8205 case OKCancel: 8206 buttons = ["OK", "Cancel"]; 8207 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 8208 break; 8209 case RetryCancel: 8210 buttons = ["Retry", "Cancel"]; 8211 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 8212 break; 8213 case YesNo: 8214 buttons = ["Yes", "No"]; 8215 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 8216 break; 8217 case YesNoCancel: 8218 buttons = ["Yes", "No", "Cancel"]; 8219 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 8220 break; 8221 case RetryCancelContinue: 8222 buttons = ["Try Again", "Cancel", "Continue"]; 8223 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 8224 break; 8225 } 8226 auto mb = new MessageBox(message, buttons, buttonIds); 8227 EventLoop el = EventLoop.get; 8228 el.run(() { return !mb.win.closed; }); 8229 return mb.buttonPressed; 8230 } 8231 } 8232 8233 /// ditto 8234 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8235 return messageBox(null, message, style, icon); 8236 } 8237 8238 8239 8240 /// 8241 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 8242 8243 /++ 8244 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 8245 8246 History: 8247 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. 8248 +/ 8249 struct EventListener { 8250 private Widget widget; 8251 private string event; 8252 private EventHandler handler; 8253 private bool useCapture; 8254 8255 /// 8256 void disconnect() { 8257 widget.removeEventListener(this); 8258 } 8259 } 8260 8261 /++ 8262 The purpose of this enum was to give a compile-time checked version of various standard event strings. 8263 8264 Now, I recommend you use a statically typed event object instead. 8265 8266 See_Also: [Event] 8267 +/ 8268 enum EventType : string { 8269 click = "click", /// 8270 8271 mouseenter = "mouseenter", /// 8272 mouseleave = "mouseleave", /// 8273 mousein = "mousein", /// 8274 mouseout = "mouseout", /// 8275 mouseup = "mouseup", /// 8276 mousedown = "mousedown", /// 8277 mousemove = "mousemove", /// 8278 8279 keydown = "keydown", /// 8280 keyup = "keyup", /// 8281 char_ = "char", /// 8282 8283 focus = "focus", /// 8284 blur = "blur", /// 8285 8286 triggered = "triggered", /// 8287 8288 change = "change", /// 8289 } 8290 8291 /++ 8292 Represents an event that is currently being processed. 8293 8294 8295 Minigui's event model is based on the web browser. An event has a name, a target, 8296 and an associated data object. It starts from the window and works its way down through 8297 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 8298 then goes back up again all the way back to the window, triggering bubble phase handlers. At 8299 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 8300 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 8301 was called; that just stops it from calling other handlers in the widget tree, but the default happens 8302 whenever propagation is done, not only if it gets to the end of the chain). 8303 8304 This model has several nice points: 8305 8306 $(LIST 8307 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 8308 with event handlers set, then add/remove children as much as you want without needing 8309 to manage the event handlers on them - the parent alone can manage everything. 8310 8311 * It is easy to create new custom events in your application. 8312 8313 * It is familiar to many web developers. 8314 ) 8315 8316 There's a few downsides though: 8317 8318 $(LIST 8319 * There's not a lot of type safety. 8320 8321 * You don't get a static list of what events a widget can emit. 8322 8323 * Tracing where an event got cancelled along the chain can get difficult; the downside of 8324 the central delegation benefit is it can be lead to debugging of action at a distance. 8325 ) 8326 8327 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 8328 while keeping the benefits - and most compatibility with - the existing model. The main idea is 8329 to simply use a D object type which provides a static interface as well as a built-in event name. 8330 Then, a new static interface allows you to see what an event can emit and attach handlers to it 8331 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 8332 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 8333 to having a little more help from the D compiler and documentation generator. 8334 8335 Your code would change like this: 8336 8337 --- 8338 // old 8339 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 8340 8341 // new 8342 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 8343 --- 8344 8345 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. 8346 8347 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 8348 8349 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 8350 8351 Thus the family of functions are: 8352 8353 [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. 8354 8355 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 8356 8357 [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. 8358 8359 Let's implement a custom widget that can emit a ChangeEvent. 8360 8361 class MyCheckbox : Widget { 8362 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 8363 /// It is NOT actually required but should be used whenever possible. 8364 mixin Emits!ChangeEvent; 8365 8366 this(Widget parent) { 8367 super(parent); 8368 setDefaultEventHandler((ClickEvent) { emit(ChangeEvent()); }); 8369 } 8370 } 8371 8372 Events can be subclasses of other events. If this is true, they trigger all their parent class events as well. 8373 8374 class StringChangeEvent : ChangeEvent {} 8375 8376 Will count as BOTH "stringchange" and "change" (and "*", the base event type). You only need to emit the most derived type. 8377 8378 ## Creating Your Own Events 8379 8380 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. 8381 8382 --- 8383 class MyEvent : Event { 8384 mixin Register; // adds EventString and other reflection information 8385 } 8386 8387 ## General Conventions 8388 8389 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. 8390 8391 History: 8392 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. 8393 8394 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. 8395 +/ 8396 /+ 8397 ## Qt-style signals and slots 8398 8399 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. 8400 8401 The intention is for events to be used when 8402 8403 --- 8404 class Demo : Widget { 8405 this() { 8406 myPropertyChanged = Signal!int(this); 8407 } 8408 @property myProperty(int v) { 8409 myPropertyChanged.emit(v); 8410 } 8411 8412 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 8413 // but it can just genuinely not care about `this` since that's not really passed. 8414 } 8415 8416 class Foo : Widget { 8417 // the slot uda is not necessary, but it helps the script and ui builder find it. 8418 @slot void setValue(int v) { ... } 8419 } 8420 8421 demo.myPropertyChanged.connect(&foo.setValue); 8422 --- 8423 8424 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 8425 8426 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 8427 8428 class StringChangeEvent : ChangeEvent, Signal!string { 8429 mixin SignalImpl 8430 } 8431 8432 +/ 8433 class Event { 8434 /// Creates an event without populating any members and without sending it. See [dispatch] 8435 this(string eventName, Widget emittedBy) { 8436 this.eventName = eventName; 8437 this.srcElement = emittedBy; 8438 } 8439 8440 /++ 8441 Events should generally follow the propagation model, but there's some exceptions 8442 to that rule. If so, they should override this to return false. In that case, only 8443 bubbling event handlers on the target itself and capturing event handlers on the containing 8444 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 8445 capture -> target -> bubble process.) 8446 8447 History: 8448 Added May 12, 2021 8449 +/ 8450 bool propagates() const pure nothrow @nogc @safe { 8451 return true; 8452 } 8453 8454 /++ 8455 hints as to whether preventDefault will actually do anything. not entirely reliable 8456 History: 8457 Added May 14, 2021 8458 +/ 8459 bool cancelable() const pure nothrow @nogc @safe { 8460 return true; 8461 } 8462 8463 /++ 8464 You can mix this into child class to register some boilerplate. It includes the `EventString` 8465 member, a constructor, and implementations of the dynamic get data interfaces. 8466 8467 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 8468 8469 8470 You can override the default EventString by simply providing your own in the form of 8471 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 8472 which provides some namespace protection against conflicts in other libraries while still being fairly 8473 easy to use. 8474 8475 If you provide your own constructor, it will override the default constructor provided here. A constructor 8476 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 8477 first argument to your constructor. 8478 8479 History: 8480 Added May 13, 2021. 8481 +/ 8482 protected static mixin template Register() { 8483 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 8484 this(Widget target) { super(EventString, target); } 8485 } 8486 8487 /++ 8488 This is the widget that emitted the event. 8489 8490 8491 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 8492 8493 History: 8494 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 8495 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 8496 so I don't intend to remove these aliases. 8497 +/ 8498 Widget source; 8499 /// ditto 8500 alias source target; 8501 /// ditto 8502 alias source srcElement; 8503 8504 Widget relatedTarget; /// Note: likely to be deprecated at some point. 8505 8506 /// Prevents the default event handler (if there is one) from being called 8507 void preventDefault() { 8508 lastDefaultPrevented = true; 8509 defaultPrevented = true; 8510 } 8511 8512 /// Stops the event propagation immediately. 8513 void stopPropagation() { 8514 propagationStopped = true; 8515 } 8516 8517 private bool defaultPrevented; 8518 private bool propagationStopped; 8519 private string eventName; 8520 8521 private bool isBubbling; 8522 8523 protected void adjustScrolling() { } 8524 8525 /++ 8526 this sends it only to the target. If you want propagation, use dispatch() instead. 8527 8528 This should be made private!!! 8529 8530 +/ 8531 void sendDirectly() { 8532 if(srcElement is null) 8533 return; 8534 8535 // 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. 8536 8537 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 8538 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 8539 8540 adjustScrolling(); 8541 8542 if(auto e = target.parentWindow) { 8543 if(auto handlers = "*" in e.capturingEventHandlers) 8544 foreach(handler; *handlers) 8545 if(handler) handler(e, this); 8546 if(auto handlers = eventName in e.capturingEventHandlers) 8547 foreach(handler; *handlers) 8548 if(handler) handler(e, this); 8549 } 8550 8551 auto e = srcElement; 8552 8553 if(auto handlers = eventName in e.bubblingEventHandlers) 8554 foreach(handler; *handlers) 8555 if(handler) handler(e, this); 8556 8557 if(auto handlers = "*" in e.bubblingEventHandlers) 8558 foreach(handler; *handlers) 8559 if(handler) handler(e, this); 8560 8561 // there's never a default for a catch-all event 8562 if(!defaultPrevented) 8563 if(eventName in e.defaultEventHandlers) 8564 e.defaultEventHandlers[eventName](e, this); 8565 } 8566 8567 /// this dispatches the element using the capture -> target -> bubble process 8568 void dispatch() { 8569 if(srcElement is null) 8570 return; 8571 8572 if(!propagates) { 8573 sendDirectly; 8574 return; 8575 } 8576 8577 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 8578 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 8579 8580 adjustScrolling(); 8581 // first capture, then bubble 8582 8583 Widget[] chain; 8584 Widget curr = srcElement; 8585 while(curr) { 8586 auto l = curr; 8587 chain ~= l; 8588 curr = curr.parent; 8589 } 8590 8591 isBubbling = false; 8592 8593 foreach_reverse(e; chain) { 8594 if(auto handlers = "*" in e.capturingEventHandlers) 8595 foreach(handler; *handlers) if(handler !is null) handler(e, this); 8596 8597 if(propagationStopped) 8598 break; 8599 8600 if(auto handlers = eventName in e.capturingEventHandlers) 8601 foreach(handler; *handlers) if(handler !is null) handler(e, this); 8602 8603 // the default on capture should really be to always do nothing 8604 8605 //if(!defaultPrevented) 8606 // if(eventName in e.defaultEventHandlers) 8607 // e.defaultEventHandlers[eventName](e.element, this); 8608 8609 if(propagationStopped) 8610 break; 8611 } 8612 8613 isBubbling = true; 8614 if(!propagationStopped) 8615 foreach(e; chain) { 8616 if(auto handlers = eventName in e.bubblingEventHandlers) 8617 foreach(handler; *handlers) if(handler !is null) handler(e, this); 8618 8619 if(propagationStopped) 8620 break; 8621 8622 if(auto handlers = "*" in e.bubblingEventHandlers) 8623 foreach(handler; *handlers) if(handler !is null) handler(e, this); 8624 8625 if(propagationStopped) 8626 break; 8627 } 8628 8629 if(!defaultPrevented) 8630 foreach(e; chain) { 8631 if(eventName in e.defaultEventHandlers) 8632 e.defaultEventHandlers[eventName](e, this); 8633 } 8634 } 8635 8636 8637 /* old compatibility things */ 8638 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward") 8639 final @property { 8640 Key key() { return (cast(KeyEventBase) this).key; } 8641 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 8642 8643 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 8644 bool altKey() { return (cast(KeyEventBase) this).altKey; } 8645 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 8646 } 8647 8648 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward") 8649 final @property { 8650 int clientX() { return (cast(MouseEventBase) this).clientX; } 8651 int clientY() { return (cast(MouseEventBase) this).clientY; } 8652 8653 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 8654 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 8655 8656 int button() { return (cast(MouseEventBase) this).button; } 8657 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 8658 } 8659 8660 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 8661 final @property { 8662 int state() { 8663 if(auto meb = cast(MouseEventBase) this) 8664 return meb.state; 8665 if(auto keb = cast(KeyEventBase) this) 8666 return keb.state; 8667 assert(0); 8668 } 8669 } 8670 8671 deprecated("Use a CharEvent instead of Event in your handler going forward") 8672 final @property { 8673 dchar character() { 8674 if(auto ce = cast(CharEvent) this) 8675 return ce.character; 8676 return dchar.init; 8677 } 8678 } 8679 8680 // for change events 8681 @property { 8682 /// 8683 int intValue() { return 0; } 8684 /// 8685 string stringValue() { return null; } 8686 } 8687 } 8688 8689 /++ 8690 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 8691 8692 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 8693 dynamic and custom events, but the static list helps ensure you get them right. 8694 8695 If this is declared, you can use [Widget.emit] to send the event. 8696 8697 All events work the same way though, following the capture->widget->bubble model described under [Event]. 8698 8699 History: 8700 Added May 4, 2021 8701 +/ 8702 mixin template Emits(EventType) { 8703 import arsd.minigui : EventString; 8704 static if(is(EventType : Event) && !is(EventType == Event)) 8705 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 8706 else 8707 static assert(0, "You can only emit subclasses of Event"); 8708 } 8709 8710 /// ditto 8711 mixin template Emits(string eventString) { 8712 mixin("private Event[0] emits_" ~ eventString ~";"); 8713 } 8714 8715 /* 8716 class SignalEvent(string name) : Event { 8717 8718 } 8719 */ 8720 8721 /++ 8722 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". 8723 8724 8725 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. 8726 8727 History: 8728 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 8729 +/ 8730 class CommandEvent : Event { 8731 enum EventString = "command"; 8732 this(Widget source, string CommandEvent = EventString) { 8733 super(CommandEvent, source); 8734 } 8735 } 8736 8737 /++ 8738 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 8739 +/ 8740 class CommandEventWithArgs(Args...) : CommandEvent { 8741 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 8742 Args args; 8743 } 8744 8745 /++ 8746 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. 8747 8748 See [CommandEvent] for more information. 8749 8750 Returns: 8751 The [EventListener] you can use to remove the handler. 8752 +/ 8753 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 8754 return w.addEventListener(CommandString, (Event ev) { 8755 if(ev.target is w) 8756 return; // it does not consume its own commands! 8757 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 8758 handler(cev.args); 8759 ev.stopPropagation(); 8760 } 8761 }); 8762 } 8763 8764 /++ 8765 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. 8766 +/ 8767 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 8768 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 8769 event.dispatch(); 8770 } 8771 8772 class ResizeEvent : Event { 8773 enum EventString = "resize"; 8774 8775 this(Widget target) { super(EventString, target); } 8776 8777 override bool propagates() const { return false; } 8778 } 8779 8780 class BlurEvent : Event { 8781 enum EventString = "blur"; 8782 8783 // FIXME: related target? 8784 this(Widget target) { super(EventString, target); } 8785 8786 override bool propagates() const { return false; } 8787 } 8788 8789 class FocusEvent : Event { 8790 enum EventString = "focus"; 8791 8792 // FIXME: related target? 8793 this(Widget target) { super(EventString, target); } 8794 } 8795 8796 class ScrollEvent : Event { 8797 enum EventString = "scroll"; 8798 this(Widget target) { super(EventString, target); } 8799 } 8800 8801 /++ 8802 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 8803 8804 History: 8805 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 8806 +/ 8807 class CharEvent : Event { 8808 enum EventString = "char"; 8809 this(Widget target, dchar ch) { 8810 character = ch; 8811 super(EventString, target); 8812 } 8813 8814 immutable dchar character; 8815 } 8816 8817 /++ 8818 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 8819 +/ 8820 abstract class ChangeEventBase : Event { 8821 enum EventString = "change"; 8822 this(Widget target) { 8823 super(EventString, target); 8824 } 8825 8826 /+ 8827 // idk where or how exactly i want to do this. 8828 // i might come back to it later. 8829 8830 // If a widget itself broadcasts one of theses itself, it stops propagation going down 8831 // this way the source doesn't get too confused (think of a nested scroll widget) 8832 // 8833 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 8834 // then you consume that command and change you scroll x position to whatever. then you do 8835 // some kind of change event that is broadcast back to the children and any horizontal scroll 8836 // listeners are now able to update, without having an explicit connection between them. 8837 void broadcastToChildren(string fieldName) { 8838 8839 } 8840 +/ 8841 } 8842 8843 /++ 8844 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. 8845 8846 8847 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). 8848 8849 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);` 8850 8851 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 8852 8853 History: 8854 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. 8855 +/ 8856 class ChangeEvent(T) : ChangeEventBase { 8857 this(Widget target, T delegate() getNewValue) { 8858 assert(getNewValue !is null); 8859 this.getNewValue = getNewValue; 8860 super(target); 8861 } 8862 8863 private T delegate() getNewValue; 8864 8865 /++ 8866 Gets the new value that just changed. 8867 +/ 8868 @property T value() { 8869 return getNewValue(); 8870 } 8871 8872 /// compatibility method for old generic Events 8873 static if(is(immutable T == immutable int)) 8874 override int intValue() { return value; } 8875 /// ditto 8876 static if(is(immutable T == immutable string)) 8877 override string stringValue() { return value; } 8878 } 8879 8880 /++ 8881 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 8882 8883 8884 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 8885 8886 History: 8887 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 8888 +/ 8889 abstract class KeyEventBase : Event { 8890 this(string name, Widget target) { 8891 super(name, target); 8892 } 8893 8894 // for key events 8895 Key key; /// 8896 8897 KeyEvent originalKeyEvent; 8898 8899 /++ 8900 Indicates the current state of the given keyboard modifier keys. 8901 8902 History: 8903 Added to events on April 15, 2020. 8904 +/ 8905 bool ctrlKey; 8906 8907 /// ditto 8908 bool altKey; 8909 8910 /// ditto 8911 bool shiftKey; 8912 8913 /++ 8914 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 8915 8916 See [arsd.simpledisplay.ModifierState] for other possible flags. 8917 +/ 8918 int state; 8919 } 8920 8921 /++ 8922 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]. 8923 8924 8925 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 8926 8927 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. 8928 8929 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. 8930 8931 See_Also: [KeyUpEvent], [CharEvent] 8932 8933 History: 8934 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 8935 +/ 8936 class KeyDownEvent : KeyEventBase { 8937 enum EventString = "keydown"; 8938 this(Widget target) { super(EventString, target); } 8939 } 8940 8941 /++ 8942 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 8943 8944 8945 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 8946 8947 See_Also: [KeyDownEvent], [CharEvent] 8948 8949 History: 8950 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 8951 +/ 8952 class KeyUpEvent : KeyEventBase { 8953 enum EventString = "keyup"; 8954 this(Widget target) { super(EventString, target); } 8955 } 8956 8957 /++ 8958 Contains shared properties for various mouse events; 8959 8960 8961 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 8962 8963 History: 8964 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 8965 +/ 8966 abstract class MouseEventBase : Event { 8967 this(string name, Widget target) { 8968 super(name, target); 8969 } 8970 8971 // for mouse events 8972 int clientX; /// The mouse event location relative to the target widget 8973 int clientY; /// ditto 8974 8975 int viewportX; /// The mouse event location relative to the window origin 8976 int viewportY; /// ditto 8977 8978 int button; /// [MouseEvent.button] 8979 int buttonLinear; /// [MouseEvent.buttonLinear] 8980 8981 int state; /// 8982 8983 override void adjustScrolling() { 8984 version(custom_widgets) { // TEMP 8985 viewportX = clientX; 8986 viewportY = clientY; 8987 if(auto se = cast(ScrollableWidget) srcElement) { 8988 clientX += se.scrollOrigin.x; 8989 clientY += se.scrollOrigin.y; 8990 } 8991 } 8992 } 8993 8994 } 8995 8996 /++ 8997 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 8998 8999 9000 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 9001 9002 [MouseUpEvent] is sent when the user releases a mouse button. 9003 9004 [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.) 9005 9006 [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. 9007 9008 [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. 9009 9010 [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. 9011 9012 [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. 9013 9014 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 9015 9016 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 9017 9018 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 9019 9020 Rationale: 9021 9022 If you only want to do drag, mousedown/up works just fine being consistently sent. 9023 9024 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). 9025 9026 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. 9027 9028 History: 9029 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. 9030 +/ 9031 class MouseUpEvent : MouseEventBase { 9032 enum EventString = "mouseup"; /// 9033 this(Widget target) { super(EventString, target); } 9034 } 9035 /// ditto 9036 class MouseDownEvent : MouseEventBase { 9037 enum EventString = "mousedown"; /// 9038 this(Widget target) { super(EventString, target); } 9039 } 9040 /// ditto 9041 class MouseMoveEvent : MouseEventBase { 9042 enum EventString = "mousemove"; /// 9043 this(Widget target) { super(EventString, target); } 9044 } 9045 /// ditto 9046 class ClickEvent : MouseEventBase { 9047 enum EventString = "click"; /// 9048 this(Widget target) { super(EventString, target); } 9049 } 9050 /// ditto 9051 class DoubleClickEvent : MouseEventBase { 9052 enum EventString = "dblclick"; /// 9053 this(Widget target) { super(EventString, target); } 9054 } 9055 /// ditto 9056 class MouseOverEvent : Event { 9057 enum EventString = "mouseover"; /// 9058 this(Widget target) { super(EventString, target); } 9059 } 9060 /// ditto 9061 class MouseOutEvent : Event { 9062 enum EventString = "mouseout"; /// 9063 this(Widget target) { super(EventString, target); } 9064 } 9065 /// ditto 9066 class MouseEnterEvent : Event { 9067 enum EventString = "mouseenter"; /// 9068 this(Widget target) { super(EventString, target); } 9069 9070 override bool propagates() const { return false; } 9071 } 9072 /// ditto 9073 class MouseLeaveEvent : Event { 9074 enum EventString = "mouseleave"; /// 9075 this(Widget target) { super(EventString, target); } 9076 9077 override bool propagates() const { return false; } 9078 } 9079 9080 private bool isAParentOf(Widget a, Widget b) { 9081 if(a is null || b is null) 9082 return false; 9083 9084 while(b !is null) { 9085 if(a is b) 9086 return true; 9087 b = b.parent; 9088 } 9089 9090 return false; 9091 } 9092 9093 private struct WidgetAtPointResponse { 9094 Widget widget; 9095 int x; 9096 int y; 9097 } 9098 9099 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 9100 assert(starting !is null); 9101 auto child = starting.getChildAtPosition(x, y); 9102 while(child) { 9103 if(child.hidden) 9104 continue; 9105 starting = child; 9106 x -= child.x; 9107 y -= child.y; 9108 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 9109 child = r.widget; 9110 if(child is starting) 9111 break; 9112 } 9113 return WidgetAtPointResponse(starting, x, y); 9114 } 9115 9116 version(win32_widgets) { 9117 import core.sys.windows.commctrl; 9118 9119 pragma(lib, "comctl32"); 9120 shared static this() { 9121 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 9122 INITCOMMONCONTROLSEX ic; 9123 ic.dwSize = cast(DWORD) ic.sizeof; 9124 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 9125 if(!InitCommonControlsEx(&ic)) { 9126 //import std.stdio; writeln("ICC failed"); 9127 } 9128 } 9129 9130 9131 // everything from here is just win32 headers copy pasta 9132 private: 9133 extern(Windows): 9134 9135 alias HANDLE HMENU; 9136 HMENU CreateMenu(); 9137 bool SetMenu(HWND, HMENU); 9138 HMENU CreatePopupMenu(); 9139 enum MF_POPUP = 0x10; 9140 enum MF_STRING = 0; 9141 9142 9143 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 9144 struct INITCOMMONCONTROLSEX { 9145 DWORD dwSize; 9146 DWORD dwICC; 9147 } 9148 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 9149 enum { 9150 IDB_STD_SMALL_COLOR, 9151 IDB_STD_LARGE_COLOR, 9152 IDB_VIEW_SMALL_COLOR = 4, 9153 IDB_VIEW_LARGE_COLOR = 5 9154 } 9155 enum { 9156 STD_CUT, 9157 STD_COPY, 9158 STD_PASTE, 9159 STD_UNDO, 9160 STD_REDOW, 9161 STD_DELETE, 9162 STD_FILENEW, 9163 STD_FILEOPEN, 9164 STD_FILESAVE, 9165 STD_PRINTPRE, 9166 STD_PROPERTIES, 9167 STD_HELP, 9168 STD_FIND, 9169 STD_REPLACE, 9170 STD_PRINT // = 14 9171 } 9172 9173 alias HANDLE HIMAGELIST; 9174 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 9175 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 9176 BOOL ImageList_Destroy(HIMAGELIST); 9177 9178 uint MAKELONG(ushort a, ushort b) { 9179 return cast(uint) ((b << 16) | a); 9180 } 9181 9182 9183 struct TBBUTTON { 9184 int iBitmap; 9185 int idCommand; 9186 BYTE fsState; 9187 BYTE fsStyle; 9188 version(Win64) 9189 BYTE[6] bReserved; 9190 else 9191 BYTE[2] bReserved; 9192 DWORD dwData; 9193 INT_PTR iString; 9194 } 9195 9196 enum { 9197 TB_ADDBUTTONSA = WM_USER + 20, 9198 TB_INSERTBUTTONA = WM_USER + 21, 9199 TB_GETIDEALSIZE = WM_USER + 99, 9200 } 9201 9202 struct SIZE { 9203 LONG cx; 9204 LONG cy; 9205 } 9206 9207 9208 enum { 9209 TBSTATE_CHECKED = 1, 9210 TBSTATE_PRESSED = 2, 9211 TBSTATE_ENABLED = 4, 9212 TBSTATE_HIDDEN = 8, 9213 TBSTATE_INDETERMINATE = 16, 9214 TBSTATE_WRAP = 32 9215 } 9216 9217 9218 9219 enum { 9220 ILC_COLOR = 0, 9221 ILC_COLOR4 = 4, 9222 ILC_COLOR8 = 8, 9223 ILC_COLOR16 = 16, 9224 ILC_COLOR24 = 24, 9225 ILC_COLOR32 = 32, 9226 ILC_COLORDDB = 254, 9227 ILC_MASK = 1, 9228 ILC_PALETTE = 2048 9229 } 9230 9231 9232 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 9233 9234 9235 enum { 9236 TB_ENABLEBUTTON = WM_USER + 1, 9237 TB_CHECKBUTTON, 9238 TB_PRESSBUTTON, 9239 TB_HIDEBUTTON, 9240 TB_INDETERMINATE, // = WM_USER + 5, 9241 TB_ISBUTTONENABLED = WM_USER + 9, 9242 TB_ISBUTTONCHECKED, 9243 TB_ISBUTTONPRESSED, 9244 TB_ISBUTTONHIDDEN, 9245 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 9246 TB_SETSTATE = WM_USER + 17, 9247 TB_GETSTATE = WM_USER + 18, 9248 TB_ADDBITMAP = WM_USER + 19, 9249 TB_DELETEBUTTON = WM_USER + 22, 9250 TB_GETBUTTON, 9251 TB_BUTTONCOUNT, 9252 TB_COMMANDTOINDEX, 9253 TB_SAVERESTOREA, 9254 TB_CUSTOMIZE, 9255 TB_ADDSTRINGA, 9256 TB_GETITEMRECT, 9257 TB_BUTTONSTRUCTSIZE, 9258 TB_SETBUTTONSIZE, 9259 TB_SETBITMAPSIZE, 9260 TB_AUTOSIZE, // = WM_USER + 33, 9261 TB_GETTOOLTIPS = WM_USER + 35, 9262 TB_SETTOOLTIPS = WM_USER + 36, 9263 TB_SETPARENT = WM_USER + 37, 9264 TB_SETROWS = WM_USER + 39, 9265 TB_GETROWS, 9266 TB_GETBITMAPFLAGS, 9267 TB_SETCMDID, 9268 TB_CHANGEBITMAP, 9269 TB_GETBITMAP, 9270 TB_GETBUTTONTEXTA, 9271 TB_REPLACEBITMAP, // = WM_USER + 46, 9272 TB_GETBUTTONSIZE = WM_USER + 58, 9273 TB_SETBUTTONWIDTH = WM_USER + 59, 9274 TB_GETBUTTONTEXTW = WM_USER + 75, 9275 TB_SAVERESTOREW = WM_USER + 76, 9276 TB_ADDSTRINGW = WM_USER + 77, 9277 } 9278 9279 extern(Windows) 9280 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 9281 9282 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 9283 9284 9285 enum { 9286 TB_SETINDENT = WM_USER + 47, 9287 TB_SETIMAGELIST, 9288 TB_GETIMAGELIST, 9289 TB_LOADIMAGES, 9290 TB_GETRECT, 9291 TB_SETHOTIMAGELIST, 9292 TB_GETHOTIMAGELIST, 9293 TB_SETDISABLEDIMAGELIST, 9294 TB_GETDISABLEDIMAGELIST, 9295 TB_SETSTYLE, 9296 TB_GETSTYLE, 9297 //TB_GETBUTTONSIZE, 9298 //TB_SETBUTTONWIDTH, 9299 TB_SETMAXTEXTROWS, 9300 TB_GETTEXTROWS // = WM_USER + 61 9301 } 9302 9303 enum { 9304 CCM_FIRST = 0x2000, 9305 CCM_LAST = CCM_FIRST + 0x200, 9306 CCM_SETBKCOLOR = 8193, 9307 CCM_SETCOLORSCHEME = 8194, 9308 CCM_GETCOLORSCHEME = 8195, 9309 CCM_GETDROPTARGET = 8196, 9310 CCM_SETUNICODEFORMAT = 8197, 9311 CCM_GETUNICODEFORMAT = 8198, 9312 CCM_SETVERSION = 0x2007, 9313 CCM_GETVERSION = 0x2008, 9314 CCM_SETNOTIFYWINDOW = 0x2009 9315 } 9316 9317 9318 enum { 9319 PBM_SETRANGE = WM_USER + 1, 9320 PBM_SETPOS, 9321 PBM_DELTAPOS, 9322 PBM_SETSTEP, 9323 PBM_STEPIT, // = WM_USER + 5 9324 PBM_SETRANGE32 = 1030, 9325 PBM_GETRANGE, 9326 PBM_GETPOS, 9327 PBM_SETBARCOLOR, // = 1033 9328 PBM_SETBKCOLOR = CCM_SETBKCOLOR 9329 } 9330 9331 enum { 9332 PBS_SMOOTH = 1, 9333 PBS_VERTICAL = 4 9334 } 9335 9336 enum { 9337 ICC_LISTVIEW_CLASSES = 1, 9338 ICC_TREEVIEW_CLASSES = 2, 9339 ICC_BAR_CLASSES = 4, 9340 ICC_TAB_CLASSES = 8, 9341 ICC_UPDOWN_CLASS = 16, 9342 ICC_PROGRESS_CLASS = 32, 9343 ICC_HOTKEY_CLASS = 64, 9344 ICC_ANIMATE_CLASS = 128, 9345 ICC_WIN95_CLASSES = 255, 9346 ICC_DATE_CLASSES = 256, 9347 ICC_USEREX_CLASSES = 512, 9348 ICC_COOL_CLASSES = 1024, 9349 ICC_STANDARD_CLASSES = 0x00004000, 9350 } 9351 9352 enum WM_USER = 1024; 9353 } 9354 9355 version(win32_widgets) 9356 pragma(lib, "comdlg32"); 9357 9358 9359 /// 9360 enum GenericIcons : ushort { 9361 None, /// 9362 // these happen to match the win32 std icons numerically if you just subtract one from the value 9363 Cut, /// 9364 Copy, /// 9365 Paste, /// 9366 Undo, /// 9367 Redo, /// 9368 Delete, /// 9369 New, /// 9370 Open, /// 9371 Save, /// 9372 PrintPreview, /// 9373 Properties, /// 9374 Help, /// 9375 Find, /// 9376 Replace, /// 9377 Print, /// 9378 } 9379 9380 /// 9381 void getOpenFileName( 9382 void delegate(string) onOK, 9383 string prefilledName = null, 9384 string[] filters = null 9385 ) 9386 { 9387 return getFileName(true, onOK, prefilledName, filters); 9388 } 9389 9390 /// 9391 void getSaveFileName( 9392 void delegate(string) onOK, 9393 string prefilledName = null, 9394 string[] filters = null 9395 ) 9396 { 9397 return getFileName(false, onOK, prefilledName, filters); 9398 } 9399 9400 void getFileName( 9401 bool openOrSave, 9402 void delegate(string) onOK, 9403 string prefilledName = null, 9404 string[] filters = null, 9405 ) 9406 { 9407 9408 version(win32_widgets) { 9409 import core.sys.windows.commdlg; 9410 /* 9411 Ofn.lStructSize = sizeof(OPENFILENAME); 9412 Ofn.hwndOwner = hWnd; 9413 Ofn.lpstrFilter = szFilter; 9414 Ofn.lpstrFile= szFile; 9415 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 9416 Ofn.lpstrFileTitle = szFileTitle; 9417 Ofn.nMaxFileTitle = sizeof(szFileTitle); 9418 Ofn.lpstrInitialDir = (LPSTR)NULL; 9419 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 9420 Ofn.lpstrTitle = szTitle; 9421 */ 9422 9423 9424 wchar[1024] file = 0; 9425 makeWindowsString(prefilledName, file[]); 9426 OPENFILENAME ofn; 9427 ofn.lStructSize = ofn.sizeof; 9428 ofn.lpstrFile = file.ptr; 9429 ofn.nMaxFile = file.length; 9430 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) { 9431 onOK(makeUtf8StringFromWindowsString(ofn.lpstrFile)); 9432 } 9433 } else version(custom_widgets) { 9434 auto picker = new FilePicker(prefilledName); 9435 picker.onOK = onOK; 9436 picker.show(); 9437 } 9438 } 9439 9440 version(custom_widgets) 9441 private 9442 class FilePicker : Dialog { 9443 void delegate(string) onOK; 9444 LineEdit lineEdit; 9445 this(string prefilledName, Window owner = null) { 9446 super(300, 200, "Choose File..."); // owner); 9447 9448 auto listWidget = new ListWidget(this); 9449 9450 lineEdit = new LineEdit(this); 9451 lineEdit.focus(); 9452 lineEdit.addEventListener(delegate(CharEvent event) { 9453 if(event.character == '\t' || event.character == '\n') 9454 event.preventDefault(); 9455 }); 9456 9457 listWidget.addEventListener(EventType.change, () { 9458 foreach(o; listWidget.options) 9459 if(o.selected) 9460 lineEdit.content = o.label; 9461 }); 9462 9463 //version(none) 9464 lineEdit.addEventListener((KeyDownEvent event) { 9465 if(event.key == Key.Tab) { 9466 listWidget.clear(); 9467 9468 string commonPrefix; 9469 auto cnt = lineEdit.content; 9470 if(cnt.length >= 2 && cnt[0 ..2] == "./") 9471 cnt = cnt[2 .. $]; 9472 9473 version(Windows) { 9474 WIN32_FIND_DATA data; 9475 WCharzBuffer search = WCharzBuffer("./" ~ cnt ~ "*"); 9476 auto handle = FindFirstFileW(search.ptr, &data); 9477 scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle); 9478 if(handle is INVALID_HANDLE_VALUE) { 9479 if(GetLastError() == ERROR_FILE_NOT_FOUND) 9480 goto file_not_found; 9481 throw new WindowsApiException("FindFirstFileW"); 9482 } 9483 } else version(Posix) { 9484 import core.sys.posix.dirent; 9485 auto dir = opendir("."); 9486 scope(exit) 9487 if(dir) closedir(dir); 9488 if(dir is null) 9489 throw new ErrnoApiException("opendir"); 9490 9491 auto dirent = readdir(dir); 9492 if(dirent is null) 9493 goto file_not_found; 9494 // filter those that don't start with it, since posix doesn't 9495 // do the * thing itself 9496 while(dirent.d_name[0 .. cnt.length] != cnt[]) { 9497 dirent = readdir(dir); 9498 if(dirent is null) 9499 goto file_not_found; 9500 } 9501 } else static assert(0); 9502 9503 while(true) { 9504 //foreach(string name; dirEntries(".", cnt ~ "*", SpanMode.shallow)) { 9505 version(Windows) { 9506 string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]); 9507 } else version(Posix) { 9508 string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup; 9509 } else static assert(0); 9510 9511 9512 listWidget.addOption(name); 9513 if(commonPrefix is null) 9514 commonPrefix = name; 9515 else { 9516 foreach(idx, char i; name) { 9517 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 9518 commonPrefix = commonPrefix[0 .. idx]; 9519 break; 9520 } 9521 } 9522 } 9523 9524 version(Windows) { 9525 auto ret = FindNextFileW(handle, &data); 9526 if(ret == 0) { 9527 if(GetLastError() == ERROR_NO_MORE_FILES) 9528 break; 9529 throw new WindowsApiException("FindNextFileW"); 9530 } 9531 } else version(Posix) { 9532 dirent = readdir(dir); 9533 if(dirent is null) 9534 break; 9535 9536 while(dirent.d_name[0 .. cnt.length] != cnt[]) { 9537 dirent = readdir(dir); 9538 if(dirent is null) 9539 break; 9540 } 9541 9542 if(dirent is null) 9543 break; 9544 } else static assert(0); 9545 } 9546 if(commonPrefix.length) 9547 lineEdit.content = commonPrefix; 9548 9549 file_not_found: 9550 event.preventDefault(); 9551 } 9552 }); 9553 9554 lineEdit.content = prefilledName; 9555 9556 auto hl = new HorizontalLayout(this); 9557 auto cancelButton = new Button("Cancel", hl); 9558 auto okButton = new Button("OK", hl); 9559 9560 recomputeChildLayout(); // FIXME hack 9561 9562 cancelButton.addEventListener(EventType.triggered, &Cancel); 9563 okButton.addEventListener(EventType.triggered, &OK); 9564 9565 this.addEventListener((KeyDownEvent event) { 9566 if(event.key == Key.Enter || event.key == Key.PadEnter) { 9567 event.preventDefault(); 9568 OK(); 9569 } 9570 if(event.key == Key.Escape) 9571 Cancel(); 9572 }); 9573 9574 } 9575 9576 override void OK() { 9577 if(onOK) 9578 onOK(lineEdit.content); 9579 close(); 9580 } 9581 } 9582 9583 /* 9584 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 9585 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 9586 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 9587 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 9588 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 9589 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 9590 http://www.sbin.org/doc/Xlib/chapt_03.html 9591 9592 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 9593 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 9594 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 9595 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 9596 */ 9597 9598 9599 // These are all for setMenuAndToolbarFromAnnotatedCode 9600 /// This item in the menu will be preceded by a separator line 9601 /// Group: generating_from_code 9602 struct separator {} 9603 deprecated("It was misspelled, use separator instead") alias seperator = separator; 9604 /// Program-wide keyboard shortcut to trigger the action 9605 /// Group: generating_from_code 9606 struct accelerator { string keyString; } 9607 /// tells which menu the action will be on 9608 /// Group: generating_from_code 9609 struct menu { string name; } 9610 /// Describes which toolbar section the action appears on 9611 /// Group: generating_from_code 9612 struct toolbar { string groupName; } 9613 /// 9614 /// Group: generating_from_code 9615 struct icon { ushort id; } 9616 /// 9617 /// Group: generating_from_code 9618 struct label { string label; } 9619 /// 9620 /// Group: generating_from_code 9621 struct hotkey { dchar ch; } 9622 /// 9623 /// Group: generating_from_code 9624 struct tip { string tip; } 9625 9626 9627 /++ 9628 Observes and allows inspection of an object via automatic gui 9629 +/ 9630 /// Group: generating_from_code 9631 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 9632 return new ObjectInspectionWindowImpl!(T)(t); 9633 } 9634 9635 class ObjectInspectionWindow : Window { 9636 this(int a, int b, string c) { 9637 super(a, b, c); 9638 } 9639 9640 abstract void readUpdatesFromObject(); 9641 } 9642 9643 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 9644 T t; 9645 this(T t) { 9646 this.t = t; 9647 9648 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 9649 9650 foreach(memberName; __traits(derivedMembers, T)) {{ 9651 alias member = I!(__traits(getMember, t, memberName))[0]; 9652 alias type = typeof(member); 9653 static if(is(type == int)) { 9654 auto le = new LabeledLineEdit(memberName ~ ": ", this); 9655 //le.addEventListener("char", (Event ev) { 9656 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 9657 //ev.preventDefault(); 9658 //}); 9659 le.addEventListener(EventType.change, (Event ev) { 9660 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 9661 }); 9662 9663 updateMemberDelegates[memberName] = () { 9664 le.content = toInternal!string(__traits(getMember, t, memberName)); 9665 }; 9666 } 9667 }} 9668 } 9669 9670 void delegate()[string] updateMemberDelegates; 9671 9672 override void readUpdatesFromObject() { 9673 foreach(k, v; updateMemberDelegates) 9674 v(); 9675 } 9676 } 9677 9678 /++ 9679 Creates a dialog based on a data structure. 9680 9681 --- 9682 dialog((YourStructure value) { 9683 // the user filled in the struct and clicked OK, 9684 // you can check the members now 9685 }); 9686 --- 9687 +/ 9688 /// Group: generating_from_code 9689 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null) { 9690 auto dg = new AutomaticDialog!T(onOK, onCancel); 9691 dg.show(); 9692 } 9693 9694 private static template I(T...) { alias I = T; } 9695 9696 9697 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 9698 if(name == "id") 9699 return allLowerCase ? name : "ID"; 9700 9701 char[160] buffer; 9702 int bufferIndex = 0; 9703 bool shouldCap = true; 9704 bool shouldSpace; 9705 bool lastWasCap; 9706 foreach(idx, char ch; name) { 9707 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 9708 9709 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 9710 if(lastWasCap) { 9711 // two caps in a row, don't change. Prolly acronym. 9712 } else { 9713 if(idx) 9714 shouldSpace = true; // new word, add space 9715 } 9716 9717 lastWasCap = true; 9718 } else { 9719 lastWasCap = false; 9720 } 9721 9722 if(shouldSpace) { 9723 buffer[bufferIndex++] = space; 9724 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 9725 shouldSpace = false; 9726 } 9727 if(shouldCap) { 9728 if(ch >= 'a' && ch <= 'z') 9729 ch -= 32; 9730 shouldCap = false; 9731 } 9732 if(allLowerCase && ch >= 'A' && ch <= 'Z') 9733 ch += 32; 9734 buffer[bufferIndex++] = ch; 9735 } 9736 return buffer[0 .. bufferIndex].idup; 9737 } 9738 9739 9740 9741 class AutomaticDialog(T) : Dialog { 9742 T t; 9743 9744 void delegate(T) onOK; 9745 void delegate() onCancel; 9746 9747 override int paddingTop() { return Window.lineHeight; } 9748 override int paddingBottom() { return Window.lineHeight; } 9749 override int paddingRight() { return Window.lineHeight; } 9750 override int paddingLeft() { return Window.lineHeight; } 9751 9752 9753 this(void delegate(T) onOK, void delegate() onCancel) { 9754 static if(is(T == class)) 9755 t = new T(); 9756 this.onOK = onOK; 9757 this.onCancel = onCancel; 9758 super(400, cast(int)(__traits(allMembers, T).length + 5) * Window.lineHeight, T.stringof); 9759 9760 foreach(memberName; __traits(allMembers, T)) { 9761 alias member = I!(__traits(getMember, t, memberName))[0]; 9762 alias type = typeof(member); 9763 static if(is(type == bool)) { 9764 auto box = new Checkbox(memberName.beautify, this); 9765 box.addEventListener(EventType.change, (Event ev) { 9766 __traits(getMember, t, memberName) = box.isChecked; 9767 }); 9768 } else static if(is(type == string)) { 9769 auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this); 9770 le.addEventListener(EventType.change, (Event ev) { 9771 __traits(getMember, t, memberName) = ev.stringValue; 9772 }); 9773 } else static if(is(type : long)) { 9774 auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this); 9775 /+ 9776 le.addEventListener("char", (Event ev) { 9777 if((ev.character < '0' || ev.character > '9') && ev.character != '-') 9778 ev.preventDefault(); 9779 }); 9780 +/ 9781 le.addEventListener(EventType.change, (Event ev) { 9782 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 9783 }); 9784 } 9785 } 9786 9787 auto hl = new HorizontalLayout(this); 9788 auto stretch = new HorizontalSpacer(hl); // to right align 9789 auto ok = new CommandButton("OK", hl); 9790 auto cancel = new CommandButton("Cancel", hl); 9791 ok.addEventListener(EventType.triggered, &OK); 9792 cancel.addEventListener(EventType.triggered, &Cancel); 9793 9794 this.addEventListener((KeyDownEvent ev) { 9795 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 9796 ok.focus(); 9797 OK(); 9798 ev.preventDefault(); 9799 } 9800 if(ev.key == Key.Escape) { 9801 Cancel(); 9802 ev.preventDefault(); 9803 } 9804 }); 9805 9806 this.children[0].focus(); 9807 } 9808 9809 override void OK() { 9810 onOK(t); 9811 close(); 9812 } 9813 9814 override void Cancel() { 9815 if(onCancel) 9816 onCancel(); 9817 close(); 9818 } 9819 } 9820 9821 private template baseClassCount(Class) { 9822 private int helper() { 9823 int count = 0; 9824 static if(is(Class bases == super)) { 9825 foreach(base; bases) 9826 static if(is(base == class)) 9827 count += 1 + baseClassCount!base; 9828 } 9829 return count; 9830 } 9831 9832 enum int baseClassCount = helper(); 9833 } 9834 9835 private long stringToLong(string s) { 9836 long ret; 9837 if(s.length == 0) 9838 return ret; 9839 bool negative = s[0] == '-'; 9840 if(negative) 9841 s = s[1 .. $]; 9842 foreach(ch; s) { 9843 if(ch >= '0' && ch <= '9') { 9844 ret *= 10; 9845 ret += ch - '0'; 9846 } 9847 } 9848 if(negative) 9849 ret = -ret; 9850 return ret; 9851 } 9852 9853 9854 /+ 9855 Both widgets and events should prolly have the get/set property thing for runtime info. 9856 9857 9858 It would use this for GUI designer access, XML construction, and scripting. 9859 9860 interface Reflectable { 9861 9862 string[] getPropertyNames(); 9863 string getProperty(string name); 9864 void setProperty(string name, string value); 9865 9866 mixin template Register() { 9867 9868 } 9869 } 9870 +/ 9871 9872 9873 /+ 9874 9875 I could fix up the hierarchy kinda like this 9876 9877 class Widget { 9878 Widget[] children() { return null; } 9879 } 9880 interface WidgetContainer { 9881 Widget asWidget(); 9882 void addChild(Widget w); 9883 9884 // alias asWidget this; // but meh 9885 } 9886 9887 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 9888 9889 class Layout : Widget, WidgetContainer {} 9890 9891 class Window : WidgetContainer {} 9892 9893 9894 All constructors that previously took Widgets should now take WidgetContainers instead 9895 9896 9897 9898 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". 9899 +/ 9900 9901 /+ 9902 LAYOUTS 2.0 9903 9904 can just be assigned as a function. assigning a new one will cause it to be immediately called. 9905 9906 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 9907 9908 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 9909 9910 and even Paint can just use computedStyle... 9911 9912 background color 9913 font 9914 border color and style 9915 9916 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 9917 please note that many widgets and in some modes will completely ignore properties as they will. 9918 they are just hints you set, not promises. 9919 9920 9921 9922 9923 9924 So generally the existing virtual functions are just the default for the class. But individual objects 9925 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 9926 +/ 9927 9928 /++ 9929 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!) 9930 9931 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. 9932 9933 You should not inherit from this directly, but instead use [VisualTheme]. 9934 9935 History: 9936 Added May 8, 2021 9937 +/ 9938 abstract class BaseVisualTheme { 9939 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 9940 void doPaint(Widget widget, WidgetPainter painter); 9941 /++ 9942 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 9943 where the interpretation of the string varies for each property and may include things like measurement units. 9944 +/ 9945 abstract string getPropertyString(Widget widget, string propertyName); 9946 9947 /++ 9948 Default background color of the window. Widgets also use this to simulate transparency. 9949 9950 Probably some shade of grey. 9951 +/ 9952 abstract Color windowBackgroundColor(); 9953 abstract Color foregroundColor(); 9954 abstract Color lightAccentColor(); 9955 abstract Color darkAccentColor(); 9956 9957 /++ 9958 Color used to indicate active selections in lists and text boxes, etc. 9959 +/ 9960 abstract Color selectionColor(); 9961 9962 abstract OperatingSystemFont defaultFont(); 9963 9964 private OperatingSystemFont defaultFontCache_; 9965 private bool defaultFontCachePopulated; 9966 private OperatingSystemFont defaultFontCached() { 9967 if(!defaultFontCachePopulated) { 9968 // FIXME: set this to false if X disconnect or if visual theme changes 9969 defaultFontCache_ = defaultFont(); 9970 defaultFontCachePopulated = true; 9971 } 9972 return defaultFontCache_; 9973 } 9974 } 9975 9976 /+ 9977 A widget should have: 9978 classList 9979 dataset 9980 attributes 9981 computedStyles 9982 state (persistent) 9983 dynamic state (focused, hover, etc) 9984 +/ 9985 9986 // visualTheme.computedStyle(this).paddingLeft 9987 9988 9989 abstract class VisualTheme(CRTP) : BaseVisualTheme { 9990 override string getPropertyString(Widget widget, string propertyName) { 9991 return null; 9992 } 9993 9994 final override void doPaint(Widget widget, WidgetPainter painter) { 9995 auto derived = cast(CRTP) cast(void*) this; 9996 9997 scope void delegate(Widget, WidgetPainter) bestMatch; 9998 int bestMatchScore; 9999 10000 static if(__traits(hasMember, CRTP, "paint")) 10001 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 10002 static if(is(typeof(overload) Params == __parameters)) { 10003 static assert(Params.length == 2); 10004 static assert(is(Params[0] : Widget)); 10005 static assert(is(Params[1] == WidgetPainter)); 10006 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); 10007 10008 alias type = Params[0]; 10009 if(cast(type) widget) { 10010 auto score = baseClassCount!type; 10011 10012 if(score > bestMatchScore) { 10013 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 10014 bestMatchScore = score; 10015 } 10016 } 10017 } else static assert(0, "paint should be a method."); 10018 } 10019 10020 if(bestMatch) 10021 bestMatch(widget, painter); 10022 else 10023 widget.paint(painter); 10024 } 10025 10026 // 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 10027 override Color windowBackgroundColor() { return Color(212, 212, 212); } 10028 override Color foregroundColor() { return Color.black; } 10029 override Color darkAccentColor() { return Color(172, 172, 172); } 10030 override Color lightAccentColor() { return Color(223, 223, 223); } 10031 override Color selectionColor() { return Color(0, 0, 128); } 10032 override OperatingSystemFont defaultFont() { return null; } // will just use the default out of simpledisplay's xfontstr 10033 10034 private static struct Cached { 10035 // i prolly want to do this 10036 } 10037 } 10038 10039 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 10040 /+ 10041 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 10042 Color windowBackgroundColor() { return Color(242, 242, 242); } 10043 Color darkAccentColor() { return windowBackgroundColor; } 10044 Color lightAccentColor() { return windowBackgroundColor; } 10045 +/ 10046 } 10047 10048 // still do layout delegation 10049 // and... split off Window from Widget.