1 /+ 2 BreakpointSplitter 3 - if not all widgets fit, it collapses to tabs 4 - if they do, you get a splitter 5 - you set priority to display things first and optional breakpoint (otherwise it uses flex basis and min width) 6 +/ 7 8 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 9 10 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 11 12 // me@arsd:~/.kde/share/config$ vim kdeglobals 13 14 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 15 16 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 17 18 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 19 20 // responsive minigui, menu search, and file open with a preview hook on the side. 21 22 // FIXME: add menu checkbox and menu icon eventually 23 24 // FOXME: look at Windows rebar control too 25 26 /* 27 28 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 29 30 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 31 */ 32 33 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 34 35 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 36 37 // FIXME: opt-in file picker widget with image support 38 39 // FIXME: number widget 40 41 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 42 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 43 44 // osx style menu search. 45 46 // would be cool for a scroll bar to have marking capabilities 47 // kinda like vim's marks just on clicks etc and visual representation 48 // generically. may be cool to add an up arrow to the bottom too 49 // 50 // leave a shadow of where you last were for going back easily 51 52 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 53 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 54 // the window. 55 56 // so what about context menus? 57 58 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 59 60 // FIXME: make the scroll thing go to bottom when the content changes. 61 62 // 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 63 64 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 65 66 67 // FIXME: add a command search thingy built in and implement tip. 68 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 69 70 // On Windows: 71 // FIXME: various labels look broken in high contrast mode 72 // FIXME: changing themes while the program is upen doesn't trigger a redraw 73 74 // add note about manifest to documentation. also icons. 75 76 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 77 // FIXME: clear the corner of scrollbars if they pop up 78 79 // minigui needs to have a stdout redirection for gui mode on windows writeln 80 81 // I kinda wanna do state reacting. sort of. idk tho 82 83 // need a viewer widget that works like a web page - arrows scroll down consistently 84 85 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 86 87 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 88 // and help info about menu items. 89 // and search in menus? 90 91 // FIXME: a scroll area event signaling when a thing comes into view might be good 92 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 93 94 // FIXME: unify Windows style line endings 95 96 /* 97 TODO: 98 99 pie menu 100 101 class Form with submit behavior -- see AutomaticDialog 102 103 disabled widgets and menu items 104 105 event cleanup 106 tooltips. 107 api improvements 108 109 margins are kinda broken, they don't collapse like they should. at least. 110 111 a table form btw would be a horizontal layout of vertical layouts holding each column 112 that would give the same width things 113 */ 114 115 /* 116 117 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 118 */ 119 120 /++ 121 minigui is a smallish GUI widget library, aiming to be on par with at least 122 HTML4 forms and a few other expected gui components. It uses native controls 123 on Windows and does its own thing on Linux (Mac is not currently supported but 124 I'm slowly working on it). 125 126 127 $(H3 Conceptual Overviews) 128 129 A gui application is made out of widgets laid out in windows that display information and respond to events from the user. They also typically have actions available in menus, and you might also want to customize the appearance. How do we do these things with minigui? Let's break it down into several categories. 130 131 $(H4 Code structure) 132 133 You will typically want to create the ui, prepare event handlers, then run an event loop. The event loop drives the program, calling your methods to respond to user activity. 134 135 --- 136 import arsd.minigui; 137 138 void main() { 139 // first, create a window, the (optional) string here is its title 140 auto window = new MainWindow("Hello, World!"); 141 142 // lay out some widgets inside the window to create the ui 143 auto name = new LabeledLineEdit("What is your name?", window); 144 auto button = new Button("Say Hello", window); 145 146 // prepare event handlers 147 button.addEventListener(EventType.triggered, () { 148 window.messageBox("Hello, " ~ name.content ~ "!"); 149 }); 150 151 // show the window and run the event loop until this window is closed 152 window.loop(); 153 } 154 --- 155 156 To compile, run `opend hello.d`, then run the generated `hello` program. 157 158 While the specifics will change, nearly all minigui applications will roughly follow this pattern. 159 160 $(TIP 161 There are two other ways to run event loops: `arsd.simpledisplay.EventLoop.get.run();` and `arsd.core.getThisThreadEventLoop().run();`. They all call the same underlying functions, but have different exit conditions - the `EventLoop.get.run()` keeps running until all top-level windows are closed, and `getThisThreadEventLoop().run` keeps running until all "tasks are resolved"; it is more abstract, supporting more than just windows. 162 163 You may call this if you don't have a single main window. 164 165 Even a basic minigui window can benefit from these if you don't have a single main window: 166 167 --- 168 import arsd.minigui; 169 170 void main() { 171 // create a struct to hold gathered info 172 struct Hello { string name; } 173 // let minigui create a dialog box to get that 174 // info from the user. If you have a main window, 175 // you'd pass that here, but it is not required 176 dialog((Hello info) { 177 // inline handler of the "OK" button 178 messageBox("Hello, " ~ info.name); 179 }); 180 181 // since there is no main window to loop on, 182 // we instead call the event loop singleton ourselves 183 EventLoop.get.run; 184 } 185 --- 186 187 This is also useful when your programs lives as a notification area (aka systray) icon instead of as a window. But let's not get too far ahead of ourselves! 188 ) 189 190 $(H4 How to lay out widgets) 191 192 To better understand the details of layout algorithms and see more available included classes, see [Layout]. 193 194 $(H5 Default layouts) 195 196 minigui windows default to a flexible vertical layout, where widgets are added, from top to bottom on the window, in the same order of you creating them, then they are sized according to layout hints on the widget itself to fill the available space. This gives a reasonably usable setup but you'll probably want to customize it. 197 198 $(TIP 199 minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off. 200 ) 201 202 Generally speaking, there are two ways to customize layouts: either subclass the widget and change its hints, or wrap it in another layout widget. You can also create your own layout classes and do it all yourself, but that's fairly complicated. Wrapping existing widgets in other layout widgets is usually the easiest way to make things work. 203 204 $(NOTE 205 minigui widgets are not supposed to overlap, but can contain children, and are always rectangular. Children are laid out as rectangles inside the parent's rectangular area. 206 ) 207 208 For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]: 209 210 --- 211 import arsd.minigui; 212 void main() { 213 auto window = new MainWindow(); 214 215 // make the layout a child of our window 216 auto hl = new HorizontalLayout(window); 217 218 // then make the widgets children of the layout 219 auto leftButton = new Button("Left", hl); 220 auto rightButton = new Button("Right", hl); 221 222 window.loop(); 223 } 224 --- 225 226 A [HorizontalLayout] works just like the default [VerticalLayout], except in the other direction. These two buttons will take up all the available vertical space, then split available horizontal space equally. 227 228 $(H5 Nesting layouts) 229 230 Nesting layouts lets you carve up the rectangle in different ways. 231 232 $(EMBED_UNITTEST layout-example) 233 234 $(H5 Special layouts) 235 236 [TabWidget] can show pages of layouts as tabs. 237 238 See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead. 239 240 $(H5 Other common layout classes) 241 242 [HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout] 243 244 $(H4 How to respond to widget events) 245 246 To better understanding the underlying event system, see [Event]. 247 248 Each widget emits its own events, which propagate up through their parents until they reach their top-level window. 249 250 $(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.) 251 252 We started this series with a [MainWindow], but only added widgets to it. MainWindows also support menus and toolbars with various keyboard shortcuts. You can construct these menus by constructing classes and calling methods, but minigui also lets you just write functions in a command object and it does the rest! 253 254 See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example. 255 256 Note that toggleable menu or toolbar items are not yet implemented, but on the todolist. Submenus and disabled items are also not supported at this time and not currently on the work list (but if you need it, let me know and MAYBE we can work something out. Emphasis on $(I maybe)). 257 258 $(TIP 259 The automatic dialog box logic is also available for you to invoke on demand with [dialog] and the data setting logic can be used with a child widget inside an existing window [addDataControllerWidget], which also has annotation-based layout capabilities. 260 ) 261 262 All windows also have titles. You can change this at any time with the `window.title = "string";` property. 263 264 Windows also have icons, which can be set with the `window.icon` property. It takes a [arsd.color.MemoryImage] object, which is an in-memory bitmap. [arsd.image] can load common file formats into these objects, or you can make one yourself. The default icon on Windows is the icon of your exe, which you can set through a resource file. (FIXME: explain how to do this easily.) 265 266 The `MainWindow` also provides a status bar across the bottom. These aren't so common in new applications, but I love them - on my own computer, I even have a global status bar for my whole desktop! I suggest you use it: a status bar is a consistent place to put information and notifications that will never overlap other content. 267 268 A status bar has parts, and the parts have content. The first part's content is assumed to change frequently; the default mouse over event will set it to [Widget.statusTip], a public `string` you can assign to any widget you want at any time. 269 270 Other parts can be added by you and are under your control. You add them with: 271 272 --- 273 window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units); 274 --- 275 276 The size can be in a variety of units and what you get with mixes can get complicated. The rule is: explicit pixel sizes are used first. Then, proportional sizes are applied to the remaining space. Then, finally, if there is any space left, any items without an explicit size split them equally. 277 278 You may prefer to set them all at once, with: 279 280 --- 281 window.statusBar.parts.setSizes(1, 1, 1); 282 --- 283 284 This makes a three-part status bar, each with the same size - they all take the same proportion of the total size. Negative numbers here will use auto-scaled pixels. 285 286 You should call this right after creating your `MainWindow` as part of your setup code. 287 288 Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";` 289 290 $(NOTE 291 I'm thinking about making the other parts do other things by default too, but if I do change it, I'll try not to break any explicitly set things you do anyway. 292 ) 293 294 If you really don't want a status bar on your main window, you can remove it with `window.statusBar = null;` Make sure you don't try to use it again, or your program will likely crash! 295 296 Status bars, at this time, cannot hold non-text content, but I do want to change that. They also cannot have event listeners at this time, but again, that is likely to change. I have something in mind where they can hold clickable messages with a history and maybe icons, but haven't implemented any of that yet. Right now, they're just a (still very useful!) display area. 297 298 $(H4 How to do custom styles) 299 300 Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s. 301 302 $(WARNING 303 These don't apply to non-custom widgets! They will use the operating system's native theme unless the documentation for that specific class says otherwise. 304 305 At this time, custom widgets gain capability in styling, but lose capability in terms of keeping all the right integrated details of the user experience and availability to accessibility and other automation tools. Evaluate if the benefit is worth the costs before making your decision. 306 307 I'd like to erase more and more of these gaps, but no promises as to when - or even if - that will ever actually happen. 308 ) 309 310 See [Widget.Style] for more information. 311 312 $(H4 Selection of categorized widgets) 313 314 $(LIST 315 * Buttons: [Button] 316 * Text display widgets: [TextLabel], [TextDisplay] 317 * Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit] 318 * Selecting multiple on/off options: [Checkbox] 319 * Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection] 320 * Getting rough numeric input: [HorizontalSlider], [VerticalSlider] 321 * Displaying data: [ImageBox], [ProgressBar], [TableView] 322 * Showing a list of editable items: [GenericListViewWidget] 323 * Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget] 324 ) 325 326 And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons]. 327 328 If none of these do what you need, you'll want to write your own. More on that in the following section. 329 330 $(H4 custom widgets - how to write your own) 331 332 See [Widget]. 333 334 If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too! 335 336 If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required. 337 338 See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points. 339 340 You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada. 341 342 You might make a compound widget out of other widgets. [Widget.encapsulatedChildren] can help hide this from the outside world (though is not necessary and might hurt some debugging!) 343 344 $(TIP 345 Compile your application with the `-debug` switch and press F12 in your window to open a web-browser-inspired debug window. It sucks right now and doesn't do a lot, but is sometimes better than nothing. 346 ) 347 348 $(H5 Timers and animations) 349 350 The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler. 351 352 I generally don't like animations in my programs, so it hasn't been a priority for me to do more than this. I also hate uis that move outside of explicit user action, so minigui kinda supports this but I'd rather you didn't. I kinda wanna do something like `requestAnimationFrame` or something but haven't yet so it is just the `Timer` class. 353 354 $(H5 Clipboard integrations, drag and drop) 355 356 GUI application users tend to expect integration with their system, so clipboard support is basically a must, and drag and drop is nice to offer too. The functions for these are provided in [arsd.simpledisplay], which is public imported from minigui, and thus available to you here too. 357 358 I'd like to think of some better abstractions to make this more automagic, but you must do it yourself when implementing your custom widgets right now. 359 360 See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay. 361 362 $(H5 Context menus) 363 364 Override [Widget.contextMenu] in your subclass. 365 366 $(H4 Coming later) 367 368 Among the unfinished features: unified selections, translateable strings, external integrations. 369 370 $(H2 Running minigui programs) 371 372 Note the environment variable ARSD_SCALING_FACTOR on Linux can set multi-monitor scaling factors. I should also read it from a root window property so it easier to do with migrations... maybe a default theme selector from there too. 373 374 $(H2 Building minigui programs) 375 376 minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and 377 [arsd.textlayouter], on which it is built. simpledisplay provides the low-level 378 interfaces and minigui builds the concept of widgets inside the windows on top of it. 379 380 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 381 It isn't hugely concerned with appearance - on Windows, it just uses the native 382 controls and native theme, and on Linux, it keeps it simple and I may change that 383 at any time, though after May 2021, you can customize some things with css-inspired 384 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 385 you can use the custom implementation there too, but... you shouldn't.) 386 387 The event model is similar to what you use in the browser with Javascript and the 388 layout engine tries to automatically fit things in, similar to a css flexbox. 389 390 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 391 `-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead 392 of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w". 393 394 Otherwise you'll get a console and possibly other visual bugs. But if you do use 395 the subsystem:windows, note that Phobos' writeln will crash the program! 396 397 HTML_To_Classes: 398 $(SMALL_TABLE 399 HTML Code | Minigui Class 400 401 `<input type="text">` | [LineEdit] 402 `<input type="password">` | [PasswordEdit] 403 `<textarea>` | [TextEdit] 404 `<select>` | [DropDownSelection] 405 `<input type="checkbox">` | [Checkbox] 406 `<input type="radio">` | [Radiobox] 407 `<button>` | [Button] 408 ) 409 410 411 Stretchiness: 412 The default is 4. You can use larger numbers for things that should 413 consume a lot of space, and lower numbers for ones that are better at 414 smaller sizes. 415 416 Overlapped_input: 417 COMING EVENTUALLY: 418 minigui will include a little bit of I/O functionality that just works 419 with the event loop. If you want to get fancy, I suggest spinning up 420 another thread and posting events back and forth. 421 422 $(H2 Add ons) 423 See the `minigui_addons` directory in the arsd repo for some add on widgets 424 you can import separately too. 425 426 $(H3 XML definitions) 427 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 428 429 $(H3 Scriptability) 430 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 431 in this documentation, it means you can call it from the script language. 432 433 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 434 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 435 436 --- 437 import arsd.minigui_xml; 438 import arsd.script; 439 440 var globals = var.emptyObject; 441 globals.makeWidgetFromString = &makeWidgetFromString; 442 443 // this now works 444 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 445 --- 446 447 More to come. 448 449 My_UI_Guidelines: 450 Note that the Linux custom widgets generally aim to be efficient on remote X network connections. 451 452 In a perfect world, you'd achieve all the following goals: 453 454 $(LIST 455 * All operations are present in the menu 456 * The operations the user wants at the moment are right where they want them 457 * All operations can be scripted 458 * The UI does not move any elements without explicit user action 459 * All numbers can be seen and typed in if wanted, even if the ui usually hides them 460 ) 461 462 $(H2 Future Directions) 463 464 I want to do some newer ideas that might not be easy to keep working fully on Windows, like adding a menu search feature and scrollbar custom marks and typing in numbers. I might make them a default part of the widget with custom, and let you provide them through a menu or something elsewhere. 465 466 History: 467 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 468 469 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 470 tag this as version 2.0. 471 472 Among the changes: 473 $(LIST 474 * 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. 475 476 See [Event] for details. 477 478 * 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. 479 480 See [DoubleClickEvent] for details. 481 482 * 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. 483 484 See [Widget.Style] for details. 485 486 * Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it. 487 488 * Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst. 489 490 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 491 492 * 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. 493 494 * Various non-breaking additions. 495 ) 496 +/ 497 module arsd.minigui; 498 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 499 500 /++ 501 This hello world sample will have an oversized button, but that's ok, you see your first window! 502 +/ 503 version(Demo) 504 unittest { 505 import arsd.minigui; 506 507 void main() { 508 auto window = new MainWindow(); 509 510 // note the parent widget is almost always passed as the last argument to a constructor 511 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 512 auto button = new Button("Close", window); 513 button.addWhenTriggered({ 514 window.close(); 515 }); 516 517 window.loop(); 518 } 519 520 main(); // exclude from docs 521 } 522 523 /++ 524 $(ID layout-example) 525 526 This example shows one way you can partition your window into a header 527 and sidebar. Here, the header and sidebar have a fixed width, while the 528 rest of the content sizes with the window. 529 530 It might be a new way of thinking about window layout to do things this 531 way - perhaps [GridLayout] more matches your style of thought - but the 532 concept here is to partition the window into sub-boxes with a particular 533 size, then partition those boxes into further boxes. 534 535 $(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.) 536 537 So to make the header, start with a child layout that has a max height. 538 It will use that space from the top, then the remaining children will 539 split the remaining area, meaning you can think of is as just being another 540 box you can split again. Keep splitting until you have the look you desire. 541 +/ 542 // https://github.com/adamdruppe/arsd/issues/310 543 version(minigui_screenshots) 544 @Screenshot("layout") 545 unittest { 546 import arsd.minigui; 547 548 // This helper class is just to help make the layout boxes visible. 549 // think of it like a <div style="background-color: whatever;"></div> in HTML. 550 class ColorWidget : Widget { 551 this(Color color, Widget parent) { 552 this.color = color; 553 super(parent); 554 } 555 Color color; 556 class Style : Widget.Style { 557 override WidgetBackground background() { return WidgetBackground(color); } 558 } 559 mixin OverrideStyle!Style; 560 } 561 562 void main() { 563 auto window = new Window; 564 565 // the key is to give it a max height. This is one way to do it: 566 auto header = new class HorizontalLayout { 567 this() { super(window); } 568 override int maxHeight() { return 50; } 569 }; 570 // this next line is a shortcut way of doing it too, but it only works 571 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 572 // is good to know how to make a new class like above anyway. 573 // auto header = new HorizontalLayout(50, window); 574 575 auto bar = new HorizontalLayout(window); 576 577 // or since this is so common, VerticalLayout and HorizontalLayout both 578 // can just take an argument in their constructor for max width/height respectively 579 580 // (could have tone this above too, but I wanted to demo both techniques) 581 auto left = new VerticalLayout(100, bar); 582 583 // and this is the main section's container. A plain Widget instance is good enough here. 584 auto container = new Widget(bar); 585 586 // and these just add color to the containers we made above for the screenshot. 587 // in a real application, you can just add your actual controls instead of these. 588 auto headerColorBox = new ColorWidget(Color.teal, header); 589 auto leftColorBox = new ColorWidget(Color.green, left); 590 auto rightColorBox = new ColorWidget(Color.purple, container); 591 592 window.loop(); 593 } 594 595 main(); // exclude from docs 596 } 597 598 599 import arsd.core; 600 import arsd.textlayouter; 601 602 alias Timer = arsd.simpledisplay.Timer; 603 public import arsd.simpledisplay; 604 /++ 605 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 606 607 History: 608 Was private until May 15, 2021. 609 +/ 610 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 611 612 version(Windows) { 613 import core.sys.windows.winnls; 614 import core.sys.windows.windef; 615 import core.sys.windows.basetyps; 616 import core.sys.windows.winbase; 617 import core.sys.windows.winuser; 618 import core.sys.windows.wingdi; 619 static import gdi = core.sys.windows.wingdi; 620 } 621 622 version(Windows) { 623 version(minigui_manifest) {} else version=minigui_no_manifest; 624 625 version(minigui_no_manifest) {} else 626 static if(__VERSION__ >= 2_083) 627 version(CRuntime_Microsoft) { // FIXME: mingw? 628 // assume we want commctrl6 whenever possible since there's really no reason not to 629 // and this avoids some of the manifest hassle 630 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 631 } 632 } 633 634 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 635 private bool lastDefaultPrevented; 636 637 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 638 alias scriptable = arsd_jsvar_compatible; 639 640 version(Windows) { 641 // use native widgets when available unless specifically asked otherwise 642 version(custom_widgets) { 643 enum bool UsingCustomWidgets = true; 644 enum bool UsingWin32Widgets = false; 645 } else { 646 version = win32_widgets; 647 enum bool UsingCustomWidgets = false; 648 enum bool UsingWin32Widgets = true; 649 } 650 // and native theming when needed 651 //version = win32_theming; 652 } else { 653 enum bool UsingCustomWidgets = true; 654 enum bool UsingWin32Widgets = false; 655 version=custom_widgets; 656 } 657 658 659 660 /* 661 662 The main goals of minigui.d are to: 663 1) Provide basic widgets that just work in a lightweight lib. 664 I basically want things comparable to a plain HTML form, 665 plus the easy and obvious things you expect from Windows 666 apps like a menu. 667 2) Use native things when possible for best functionality with 668 least library weight. 669 3) Give building blocks to provide easy extension for your 670 custom widgets, or hooking into additional native widgets 671 I didn't wrap. 672 4) Provide interfaces for easy interaction between third 673 party minigui extensions. (event model, perhaps 674 signals/slots, drop-in ease of use bits.) 675 5) Zero non-system dependencies, including Phobos as much as 676 I reasonably can. It must only import arsd.color and 677 my simpledisplay.d. If you need more, it will have to be 678 an extension module. 679 6) An easy layout system that generally works. 680 681 A stretch goal is to make it easy to make gui forms with code, 682 some kind of resource file (xml?) and even a wysiwyg designer. 683 684 Another stretch goal is to make it easy to hook data into the gui, 685 including from reflection. So like auto-generate a form from a 686 function signature or struct definition, or show a list from an 687 array that automatically updates as the array is changed. Then, 688 your program focuses on the data more than the gui interaction. 689 690 691 692 STILL NEEDED: 693 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 694 * slider 695 * listbox 696 * spinner 697 * label? 698 * rich text 699 */ 700 701 702 /+ 703 enum LayoutMethods { 704 verticalFlex, 705 horizontalFlex, 706 inlineBlock, // left to right, no stretch, goes to next line as needed 707 static, // just set to x, y 708 verticalNoStretch, // browser style default 709 710 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 711 712 grid, // magic 713 } 714 +/ 715 716 /++ 717 The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own. 718 719 720 To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional. 721 722 --- 723 class MinimalWidget : Widget { 724 this(Widget parent) { 725 super(parent); 726 } 727 } 728 --- 729 730 $(SIDEBAR 731 I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway. 732 ) 733 734 Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal. 735 736 Among the things you'll most likely want to change in your custom widget: 737 738 $(LIST 739 * In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.) 740 741 You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now. 742 743 Do this $(I after) calling the `super` constructor. 744 745 * Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget. 746 747 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 748 749 * Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model. 750 751 * You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want. 752 ) 753 754 On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s. 755 756 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 757 758 Your own custom-drawn and native system controls can exist side-by-side. 759 760 Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas. 761 +/ 762 class Widget : ReflectableProperties { 763 764 private bool willDraw() { 765 return true; 766 } 767 768 /+ 769 /++ 770 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 771 772 History: 773 Added September 15, 2021 774 implemented.... ??? 775 +/ 776 void prepareReflection(this This)() { 777 778 } 779 +/ 780 781 private bool _enabled = true; 782 783 /++ 784 Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled. 785 786 I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it. 787 788 History: 789 Added November 23, 2021 (dub v10.4) 790 791 Warning: the specific behavior of disabling with parents may change in the future. 792 Bugs: 793 Currently only implemented for widgets backed by native Windows controls. 794 795 See_Also: [disabledReason], [disabledBy] 796 +/ 797 @property bool enabled() { 798 return disabledBy() is null; 799 } 800 801 /// ditto 802 @property void enabled(bool yes) { 803 _enabled = yes; 804 version(win32_widgets) { 805 if(hwnd) 806 EnableWindow(hwnd, yes); 807 } 808 setDynamicState(DynamicState.disabled, yes); 809 } 810 811 private string disabledReason_; 812 813 /++ 814 If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control. 815 816 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 817 818 History: 819 Added November 23, 2021 (dub v10.4) 820 See_Also: [enabled], [disabledBy] 821 +/ 822 @property string disabledReason() { 823 auto w = disabledBy(); 824 return (w is null) ? null : w.disabledReason_; 825 } 826 827 /// ditto 828 @property void disabledReason(string reason) { 829 disabledReason_ = reason; 830 } 831 832 /++ 833 Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user. 834 835 History: 836 Added November 25, 2021 (dub v10.4) 837 See_Also: [enabled], [disabledReason] 838 +/ 839 Widget disabledBy() { 840 Widget p = this; 841 while(p) { 842 if(!p._enabled) 843 return p; 844 p = p.parent; 845 } 846 return null; 847 } 848 849 /// Implementations of [ReflectableProperties] interface. See the interface for details. 850 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 851 if(valueIsJson) 852 return SetPropertyResult.wrongFormat; 853 switch(name) { 854 case "name": 855 this.name = value.idup; 856 return SetPropertyResult.success; 857 case "statusTip": 858 this.statusTip = value.idup; 859 return SetPropertyResult.success; 860 default: 861 return SetPropertyResult.noSuchProperty; 862 } 863 } 864 /// ditto 865 void getPropertiesList(scope void delegate(string name) sink) const { 866 sink("name"); 867 sink("statusTip"); 868 } 869 /// ditto 870 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 871 switch(name) { 872 case "name": 873 sink(name, this.name, false); 874 return; 875 case "statusTip": 876 sink(name, this.statusTip, false); 877 return; 878 default: 879 sink(name, null, true); 880 } 881 } 882 883 /++ 884 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 885 886 History: 887 Added November 25, 2021 (dub v10.5) 888 `Point` overload added January 12, 2022 (dub v10.6) 889 +/ 890 int scaleWithDpi(int value, int assumedDpi = 96) { 891 // avoid potential overflow with common special values 892 if(value == int.max) 893 return int.max; 894 if(value == int.min) 895 return int.min; 896 if(value == 0) 897 return 0; 898 return value * currentDpi(assumedDpi) / assumedDpi; 899 } 900 901 /// ditto 902 Point scaleWithDpi(Point value, int assumedDpi = 96) { 903 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 904 } 905 906 /++ 907 Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor. 908 909 Not entirely stable. 910 911 History: 912 Added August 25, 2023 (dub v11.1) 913 +/ 914 final int currentDpi(int assumedDpi = 96) { 915 // assert(parentWindow !is null); 916 // assert(parentWindow.win !is null); 917 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 918 //divide = 138; // to test 1.5x 919 // for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down. 920 // this also covers the case when actualDpi returns 0. 921 if(divide < 96) 922 divide = 96; 923 return divide; 924 } 925 926 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 927 // I'll think up something better eventually 928 929 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 930 protected final int defaultLineHeight() { 931 auto cs = getComputedStyle(); 932 if(cs.font && !cs.font.isNull) 933 return cs.font.height() * 5 / 4; 934 else 935 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 936 } 937 938 /++ 939 940 History: 941 Added August 25, 2023 (dub v11.1) 942 +/ 943 protected final int defaultTextHeight(int numberOfLines = 1) { 944 auto cs = getComputedStyle(); 945 if(cs.font && !cs.font.isNull) 946 return cs.font.height() * numberOfLines; 947 else 948 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 949 } 950 951 protected final int defaultTextWidth(const(char)[] text) { 952 auto cs = getComputedStyle(); 953 if(cs.font && !cs.font.isNull) 954 return cs.font.stringWidth(text); 955 else 956 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 957 } 958 959 /++ 960 If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget. 961 962 The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you. 963 964 History: 965 Added May 22, 2021 966 +/ 967 protected bool encapsulatedChildren() { 968 return false; 969 } 970 971 private void privateDpiChanged() { 972 dpiChanged(); 973 foreach(child; children) 974 child.privateDpiChanged(); 975 } 976 977 /++ 978 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 979 980 History: 981 Added January 12, 2022 (dub v10.6) 982 +/ 983 protected void dpiChanged() { 984 985 } 986 987 // Default layout properties { 988 989 int minWidth() { return 0; } 990 int minHeight() { 991 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 992 int sum = this.paddingTop + this.paddingBottom; 993 foreach(child; children) { 994 if(child.hidden) 995 continue; 996 sum += child.minHeight(); 997 sum += child.marginTop(); 998 sum += child.marginBottom(); 999 } 1000 1001 return sum; 1002 } 1003 int maxWidth() { return int.max; } 1004 int maxHeight() { return int.max; } 1005 int widthStretchiness() { return 4; } 1006 int heightStretchiness() { return 4; } 1007 1008 /++ 1009 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 1010 1011 History: 1012 Added June 15, 2021 (dub v10.1) 1013 +/ 1014 int widthShrinkiness() { return 0; } 1015 /// ditto 1016 int heightShrinkiness() { return 0; } 1017 1018 /++ 1019 The initial size of the widget for layout calculations. Default is 0. 1020 1021 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 1022 1023 History: 1024 Added June 15, 2021 (dub v10.1) 1025 +/ 1026 int flexBasisWidth() { return 0; } 1027 /// ditto 1028 int flexBasisHeight() { return 0; } 1029 1030 /++ 1031 Not stable. 1032 1033 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 1034 1035 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 1036 1037 History: 1038 Added January 5, 2023 1039 +/ 1040 Rectangle defaultMargin; 1041 /// ditto 1042 Rectangle defaultPadding; 1043 1044 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 1045 int marginRight() { return scaleWithDpi(defaultMargin.right); } 1046 int marginTop() { return scaleWithDpi(defaultMargin.top); } 1047 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 1048 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 1049 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 1050 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 1051 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 1052 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 1053 1054 private bool recomputeChildLayoutRequired = true; 1055 private static class RecomputeEvent {} 1056 private __gshared rce = new RecomputeEvent(); 1057 protected final void queueRecomputeChildLayout() { 1058 recomputeChildLayoutRequired = true; 1059 1060 if(this.parentWindow) { 1061 auto sw = this.parentWindow.win; 1062 assert(sw !is null); 1063 if(!sw.eventQueued!RecomputeEvent) { 1064 sw.postEvent(rce); 1065 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1066 } 1067 } 1068 1069 } 1070 1071 protected final void recomputeChildLayoutEntry() { 1072 if(recomputeChildLayoutRequired) { 1073 recomputeChildLayout(); 1074 recomputeChildLayoutRequired = false; 1075 redraw(); 1076 } else { 1077 // I still need to check the tree just in case one of them was queued up 1078 // and the event came up here instead of there. 1079 foreach(child; children) 1080 child.recomputeChildLayoutEntry(); 1081 } 1082 } 1083 1084 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 1085 void recomputeChildLayout() { 1086 .recomputeChildLayout!"height"(this); 1087 } 1088 1089 // } 1090 1091 1092 /++ 1093 Returns the style's tag name string this object uses. 1094 1095 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. 1096 1097 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 1098 1099 History: 1100 Added May 10, 2021 1101 +/ 1102 string styleTagName() const { 1103 string n = typeid(this).name; 1104 foreach_reverse(idx, ch; n) 1105 if(ch == '.') { 1106 n = n[idx + 1 .. $]; 1107 break; 1108 } 1109 return n; 1110 } 1111 1112 /// API for the [styleClassList] 1113 static struct ClassList { 1114 private Widget widget; 1115 1116 /// 1117 void add(string s) { 1118 widget.styleClassList_ ~= s; 1119 } 1120 1121 /// 1122 void remove(string s) { 1123 foreach(idx, s1; widget.styleClassList_) 1124 if(s1 == s) { 1125 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 1126 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 1127 widget.styleClassList_.assumeSafeAppend(); 1128 return; 1129 } 1130 } 1131 1132 /// Returns true if it was added, false if it was removed. 1133 bool toggle(string s) { 1134 if(contains(s)) { 1135 remove(s); 1136 return false; 1137 } else { 1138 add(s); 1139 return true; 1140 } 1141 } 1142 1143 /// 1144 bool contains(string s) const { 1145 foreach(s1; widget.styleClassList_) 1146 if(s1 == s) 1147 return true; 1148 return false; 1149 1150 } 1151 } 1152 1153 private string[] styleClassList_; 1154 1155 /++ 1156 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. 1157 1158 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 1159 1160 History: 1161 Added May 10, 2021 1162 +/ 1163 inout(ClassList) styleClassList() inout { 1164 return cast(inout(ClassList)) ClassList(cast() this); 1165 } 1166 1167 /++ 1168 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. 1169 1170 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. 1171 1172 The upper 32 bits are available for your own extensions. 1173 1174 History: 1175 Added May 10, 2021 1176 +/ 1177 enum DynamicState : ulong { 1178 focus = (1 << 0), /// the widget currently has the keyboard focus 1179 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 1180 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 1181 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 1182 checked = (1 << 4), /// the widget is toggleable and currently toggled on 1183 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 1184 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 1185 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 1186 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. 1187 1188 USER_BEGIN = (1UL << 32), 1189 } 1190 1191 // I want to add the primary and cancel styles to buttons at least at some point somehow. 1192 1193 /// ditto 1194 @property ulong dynamicState() { return dynamicState_; } 1195 /// ditto 1196 @property ulong dynamicState(ulong newValue) { 1197 if(dynamicState != newValue) { 1198 auto old = dynamicState_; 1199 dynamicState_ = newValue; 1200 1201 useStyleProperties((scope Widget.Style s) { 1202 if(s.variesWithState(old ^ newValue)) 1203 redraw(); 1204 }); 1205 } 1206 return dynamicState_; 1207 } 1208 1209 /// ditto 1210 void setDynamicState(ulong flags, bool state) { 1211 auto ds = dynamicState_; 1212 if(state) 1213 ds |= flags; 1214 else 1215 ds &= ~flags; 1216 1217 dynamicState = ds; 1218 } 1219 1220 private ulong dynamicState_; 1221 1222 deprecated("Use dynamic styles instead now") { 1223 Color backgroundColor() { return backgroundColor_; } 1224 void backgroundColor(Color c){ this.backgroundColor_ = c; } 1225 1226 MouseCursor cursor() { return GenericCursor.Default; } 1227 } private Color backgroundColor_ = Color.transparent; 1228 1229 1230 /++ 1231 Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such). 1232 1233 It is here so there can be a specificity switch. 1234 1235 See [OverrideStyle] for a helper function to use your own. 1236 1237 History: 1238 Added May 11, 2021 1239 +/ 1240 static class Style/* : StyleProperties*/ { 1241 public Widget widget; // public because the mixin template needs access to it 1242 1243 /++ 1244 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 1245 1246 History: 1247 Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect. 1248 +/ 1249 bool variesWithState(ulong dynamicStateFlags) { 1250 version(win32_widgets) { 1251 if(widget.hwnd) 1252 return false; 1253 } 1254 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 1255 } 1256 1257 /// 1258 Color foregroundColor() { 1259 return WidgetPainter.visualTheme.foregroundColor; 1260 } 1261 1262 /// 1263 WidgetBackground background() { 1264 // the default is a "transparent" background, which means 1265 // it goes as far up as it can to get the color 1266 if (widget.backgroundColor_ != Color.transparent) 1267 return WidgetBackground(widget.backgroundColor_); 1268 if (widget.parent) 1269 return widget.parent.getComputedStyle.background; 1270 return WidgetBackground(widget.backgroundColor_); 1271 } 1272 1273 private static OperatingSystemFont fontCached_; 1274 private OperatingSystemFont fontCached() { 1275 if(fontCached_ is null) 1276 fontCached_ = font(); 1277 return fontCached_; 1278 } 1279 1280 /++ 1281 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. 1282 +/ 1283 OperatingSystemFont font() { 1284 return null; 1285 } 1286 1287 /++ 1288 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. 1289 1290 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1291 1292 History: 1293 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1294 +/ 1295 MouseCursor cursor() { 1296 return GenericCursor.Default; 1297 } 1298 1299 FrameStyle borderStyle() { 1300 return FrameStyle.none; 1301 } 1302 1303 /++ 1304 +/ 1305 Color borderColor() { 1306 return Color.transparent; 1307 } 1308 1309 FrameStyle outlineStyle() { 1310 if(widget.dynamicState & DynamicState.focus) 1311 return FrameStyle.dotted; 1312 else 1313 return FrameStyle.none; 1314 } 1315 1316 Color outlineColor() { 1317 return foregroundColor; 1318 } 1319 } 1320 1321 /++ 1322 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1323 The basic usage is simple: 1324 1325 --- 1326 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1327 // override style hints as-needed here 1328 } 1329 OverrideStyle!Style; // add the method 1330 --- 1331 1332 $(TIP 1333 While the class is not forced to be `static`, for best results, it should be. A non-static class 1334 can not be inherited by other objects whereas the static one can. A property on the base class, 1335 called [Widget.Style.widget|widget], is available for you to access its properties. 1336 ) 1337 1338 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1339 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1340 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1341 1342 1343 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1344 You may also just override `variesWithState` when you use this flag. 1345 1346 --- 1347 mixin OverrideStyle!( 1348 DynamicState.focus, YourFocusedStyle, 1349 DynamicState.hover, YourHoverStyle, 1350 YourDefaultStyle 1351 ) 1352 --- 1353 1354 It checks if `dynamicState` matches the state and if so, returns the object given. 1355 1356 If there is no state mask given, the next one matches everything. The first match given is used. 1357 1358 However, since in most cases you'll want check state inside your individual methods, you probably won't 1359 find much use for this whole-class swap out. 1360 1361 History: 1362 Added May 16, 2021 1363 +/ 1364 static protected mixin template OverrideStyle(S...) { 1365 static import amg = arsd.minigui; 1366 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1367 ulong mask = 0; 1368 foreach(idx, thing; S) { 1369 static if(is(typeof(thing) : ulong)) { 1370 mask = thing; 1371 } else { 1372 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1373 //static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class."); 1374 scope amg.Widget.Style s = new thing(); 1375 s.widget = this; 1376 dg(s); 1377 return; 1378 } 1379 } 1380 } 1381 } 1382 } 1383 /++ 1384 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1385 +/ 1386 void useStyleProperties(scope void delegate(scope Style props) dg) { 1387 scope Style s = new Style(); 1388 s.widget = this; 1389 dg(s); 1390 } 1391 1392 1393 protected void sendResizeEvent() { 1394 this.emit!ResizeEvent(); 1395 } 1396 1397 /++ 1398 Override this to provide a custom context menu for your widget. (x, y) is where the menu was requested. If x == -1 && y == -1, the menu was triggered by the keyboard instead of the mouse and it should use the current cursor, selection, or whatever would make sense for where a keyboard user's attention would currently be. 1399 1400 It should return an instance of the [Menu] object. You may choose to cache this object. To construct one, either make `new Menu("", this);` (the empty string there is the menu's label, but for a context menu, that is not important), then call the `menu.addItem(new Action("Label Text", 0 /* icon id */, () { on clicked handler }), menu);` and `menu.addSeparator() methods, or use `return createContextMenuFromAnnotatedCode(this, some_command_struct);` 1401 1402 Context menus are automatically triggered by default by the keyboard menu key, mouse right click, and possibly other conventions per platform. You can also invoke one by calling the [showContextMenu] method. 1403 1404 See_Also: 1405 [createContextMenuFromAnnotatedCode] 1406 +/ 1407 Menu contextMenu(int x, int y) { return null; } 1408 1409 /++ 1410 Shows the widget's context menu, as if the user right clicked at the x, y position. You should rarely, if ever, have to call this, since default event handlers will do it for you automatically. To control what menu shows up, override [contextMenu] instead. 1411 +/ 1412 final bool showContextMenu(int x, int y) { 1413 return showContextMenu(x, y, -2, -2); 1414 } 1415 1416 private final bool showContextMenu(int x, int y, int screenX, int screenY) { 1417 if(parentWindow is null || parentWindow.win is null) return false; 1418 1419 auto menu = this.contextMenu(x, y); 1420 if(menu is null) 1421 return false; 1422 1423 version(win32_widgets) { 1424 // FIXME: if it is -1, -1, do it at the current selection location instead 1425 // tho the corner of the window, which it does now, isn't the literal worst. 1426 1427 // i see notepad just seems to put it in the center of the window so idk 1428 1429 if(screenX < 0 && screenY < 0) { 1430 auto p = this.globalCoordinates(); 1431 if(screenX == -2) 1432 p.x += x; 1433 if(screenY == -2) 1434 p.y += y; 1435 1436 screenX = p.x; 1437 screenY = p.y; 1438 } 1439 1440 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1441 throw new Exception("TrackContextMenuEx"); 1442 } else version(custom_widgets) { 1443 menu.popup(this, x, y); 1444 } 1445 1446 return true; 1447 } 1448 1449 /++ 1450 Removes this widget from its parent. 1451 1452 History: 1453 `removeWidget` was made `final` on May 11, 2021. 1454 +/ 1455 @scriptable 1456 final void removeWidget() { 1457 auto p = this.parent; 1458 if(p) { 1459 int item; 1460 for(item = 0; item < p._children.length; item++) 1461 if(p._children[item] is this) 1462 break; 1463 auto idx = item; 1464 for(; item < p._children.length - 1; item++) 1465 p._children[item] = p._children[item + 1]; 1466 p._children = p._children[0 .. $-1]; 1467 1468 this.parent.widgetRemoved(idx, this); 1469 //this.parent = null; 1470 1471 p.queueRecomputeChildLayout(); 1472 } 1473 version(win32_widgets) { 1474 removeAllChildren(); 1475 if(hwnd) { 1476 DestroyWindow(hwnd); 1477 hwnd = null; 1478 } 1479 } 1480 } 1481 1482 /++ 1483 Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync. 1484 1485 History: 1486 Added September 19, 2021 1487 +/ 1488 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1489 1490 /++ 1491 Removes all child widgets from `this`. You should not use the removed widgets again. 1492 1493 Note that on Windows, it also destroys the native handles for the removed children recursively. 1494 1495 History: 1496 Added July 1, 2021 (dub v10.2) 1497 +/ 1498 void removeAllChildren() { 1499 version(win32_widgets) 1500 foreach(child; _children) { 1501 child.removeAllChildren(); 1502 if(child.hwnd) { 1503 DestroyWindow(child.hwnd); 1504 child.hwnd = null; 1505 } 1506 } 1507 auto orig = this._children; 1508 this._children = null; 1509 foreach(idx, w; orig) 1510 this.widgetRemoved(idx, w); 1511 1512 queueRecomputeChildLayout(); 1513 } 1514 1515 /++ 1516 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1517 +/ 1518 @scriptable 1519 Widget getChildByName(string name) { 1520 return getByName(name); 1521 } 1522 /++ 1523 Finds the nearest descendant with the requested type and [name]. May return `this`. 1524 +/ 1525 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1526 if(this.name == name) 1527 if(auto c = cast(WidgetClass) this) 1528 return c; 1529 foreach(child; children) { 1530 auto w = child.getByName(name); 1531 if(auto c = cast(WidgetClass) w) 1532 return c; 1533 } 1534 return null; 1535 } 1536 1537 /++ 1538 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. 1539 Names should be unique in a window. 1540 1541 See_Also: [getByName], [getChildByName] 1542 +/ 1543 @scriptable string name; 1544 1545 private EventHandler[][string] bubblingEventHandlers; 1546 private EventHandler[][string] capturingEventHandlers; 1547 1548 /++ 1549 Default event handlers. These are called on the appropriate 1550 event unless [Event.preventDefault] is called on the event at 1551 some point through the bubbling process. 1552 1553 1554 If you are implementing your own widget and want to add custom 1555 events, you should follow the same pattern here: create a virtual 1556 function named `defaultEventHandler_eventname` with the implementation, 1557 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1558 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1559 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1560 This ensures virtual dispatch based on the correct subclass. 1561 1562 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1563 overridden version. 1564 1565 You only need to do that on parent classes adding NEW event types. If you 1566 just want to change the default behavior of an existing event type in a subclass, 1567 you override the function (and optionally call `super.method_name`) like normal. 1568 1569 +/ 1570 protected EventHandler[string] defaultEventHandlers; 1571 1572 /// ditto 1573 void setupDefaultEventHandlers() { 1574 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1575 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1576 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1577 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1578 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1579 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1580 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1581 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1582 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1583 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1584 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1585 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1586 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1587 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1588 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1589 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1590 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1591 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1592 } 1593 1594 /// ditto 1595 void defaultEventHandler_click(ClickEvent event) {} 1596 /// ditto 1597 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1598 /// ditto 1599 void defaultEventHandler_keydown(KeyDownEvent event) {} 1600 /// ditto 1601 void defaultEventHandler_keyup(KeyUpEvent event) {} 1602 /// ditto 1603 void defaultEventHandler_mousedown(MouseDownEvent event) { 1604 if(event.button == MouseButton.left) { 1605 if(this.tabStop) { 1606 this.focus(); 1607 } 1608 } else if(event.button == MouseButton.right) { 1609 showContextMenu(event.clientX, event.clientY); 1610 } 1611 } 1612 /// ditto 1613 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1614 /// ditto 1615 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1616 /// ditto 1617 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1618 /// ditto 1619 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1620 /// ditto 1621 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1622 /// ditto 1623 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1624 /// ditto 1625 void defaultEventHandler_char(CharEvent event) {} 1626 /// ditto 1627 void defaultEventHandler_triggered(Event event) {} 1628 /// ditto 1629 void defaultEventHandler_change(Event event) {} 1630 /// ditto 1631 void defaultEventHandler_focus(Event event) {} 1632 /// ditto 1633 void defaultEventHandler_blur(Event event) {} 1634 /// ditto 1635 void defaultEventHandler_focusin(Event event) {} 1636 /// ditto 1637 void defaultEventHandler_focusout(Event event) {} 1638 1639 /++ 1640 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1641 1642 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1643 1644 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1645 of participating in handler delegation. 1646 1647 $(TIP 1648 Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need. 1649 ) 1650 +/ 1651 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1652 return addEventListener(event, (Widget, scope Event e) { 1653 if(e.srcElement is this) 1654 handler(); 1655 }, useCapture); 1656 } 1657 1658 /// ditto 1659 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1660 return addEventListener(event, (Widget, Event e) { 1661 if(e.srcElement is this) 1662 handler(e); 1663 }, useCapture); 1664 } 1665 1666 /// ditto 1667 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1668 static if(is(Handler Fn == delegate)) { 1669 static if(is(Fn Params == __parameters)) { 1670 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1671 if(e.srcElement !is this) 1672 return; 1673 auto ty = cast(Params[0]) e; 1674 if(ty !is null) 1675 handler(ty); 1676 }, useCapture); 1677 } else static assert(0); 1678 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1679 } 1680 1681 /// ditto 1682 @scriptable 1683 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1684 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1685 } 1686 1687 /// ditto 1688 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1689 static if(is(Handler Fn == delegate)) { 1690 static if(is(Fn Params == __parameters)) { 1691 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1692 auto ty = cast(Params[0]) e; 1693 if(ty !is null) 1694 handler(ty); 1695 }, useCapture); 1696 } else static assert(0); 1697 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1698 } 1699 1700 /// ditto 1701 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1702 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1703 } 1704 1705 /// ditto 1706 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1707 if(event.length > 2 && event[0..2] == "on") 1708 event = event[2 .. $]; 1709 1710 if(useCapture) 1711 capturingEventHandlers[event] ~= handler; 1712 else 1713 bubblingEventHandlers[event] ~= handler; 1714 1715 return EventListener(this, event, handler, useCapture); 1716 } 1717 1718 /// ditto 1719 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1720 if(event.length > 2 && event[0..2] == "on") 1721 event = event[2 .. $]; 1722 1723 if(useCapture) { 1724 if(event in capturingEventHandlers) 1725 foreach(ref evt; capturingEventHandlers[event]) 1726 if(evt is handler) evt = null; 1727 } else { 1728 if(event in bubblingEventHandlers) 1729 foreach(ref evt; bubblingEventHandlers[event]) 1730 if(evt is handler) evt = null; 1731 } 1732 } 1733 1734 /// ditto 1735 void removeEventListener(EventListener listener) { 1736 removeEventListener(listener.event, listener.handler, listener.useCapture); 1737 } 1738 1739 static if(UsingSimpledisplayX11) { 1740 void discardXConnectionState() { 1741 foreach(child; children) 1742 child.discardXConnectionState(); 1743 } 1744 1745 void recreateXConnectionState() { 1746 foreach(child; children) 1747 child.recreateXConnectionState(); 1748 redraw(); 1749 } 1750 } 1751 1752 /++ 1753 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1754 1755 History: 1756 `globalCoordinates` was made `final` on May 11, 2021. 1757 +/ 1758 Point globalCoordinates() { 1759 int x = this.x; 1760 int y = this.y; 1761 auto p = this.parent; 1762 while(p) { 1763 x += p.x; 1764 y += p.y; 1765 p = p.parent; 1766 } 1767 1768 static if(UsingSimpledisplayX11) { 1769 auto dpy = XDisplayConnection.get; 1770 arsd.simpledisplay.Window dummyw; 1771 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1772 } else version(Windows) { 1773 POINT pt; 1774 pt.x = x; 1775 pt.y = y; 1776 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1777 x = pt.x; 1778 y = pt.y; 1779 } else { 1780 featureNotImplemented(); 1781 } 1782 1783 return Point(x, y); 1784 } 1785 1786 version(win32_widgets) 1787 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1788 1789 version(win32_widgets) 1790 /// Called when a WM_COMMAND is sent to the associated hwnd. 1791 void handleWmCommand(ushort cmd, ushort id) {} 1792 1793 version(win32_widgets) 1794 /++ 1795 Called when a WM_NOTIFY is sent to the associated hwnd. 1796 1797 History: 1798 +/ 1799 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1800 1801 version(win32_widgets) 1802 deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); } 1803 1804 /++ 1805 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1806 1807 Updates to this variable will only be made visible on the next mouse enter event. 1808 +/ 1809 @scriptable string statusTip; 1810 // string toolTip; 1811 // string helpText; 1812 1813 /++ 1814 If true, this widget can be focused via keyboard control with the tab key. 1815 1816 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1817 +/ 1818 bool tabStop = true; 1819 /++ 1820 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.) 1821 +/ 1822 int tabOrder; 1823 1824 version(win32_widgets) { 1825 static Widget[HWND] nativeMapping; 1826 /// The native handle, if there is one. 1827 HWND hwnd; 1828 WNDPROC originalWindowProcedure; 1829 1830 SimpleWindow simpleWindowWrappingHwnd; 1831 1832 // please note it IGNORES your return value and does NOT forward it to Windows! 1833 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1834 return 0; 1835 } 1836 } 1837 private bool implicitlyCreated; 1838 1839 /// 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. 1840 int x; 1841 /// ditto 1842 int y; 1843 private int _width; 1844 private int _height; 1845 private Widget[] _children; 1846 private Widget _parent; 1847 private Window _parentWindow; 1848 1849 /++ 1850 Returns the window to which this widget is attached. 1851 1852 History: 1853 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. 1854 +/ 1855 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1856 private @property void parentWindow(Window parent) { 1857 auto old = _parentWindow; 1858 _parentWindow = parent; 1859 newParentWindow(old, _parentWindow); 1860 foreach(child; children) 1861 child.parentWindow = parent; // please note that this is recursive 1862 } 1863 1864 /++ 1865 Called when the widget has been added to or remove from a parent window. 1866 1867 Note that either oldParent and/or newParent may be null any time this is called. 1868 1869 History: 1870 Added September 13, 2024 1871 +/ 1872 protected void newParentWindow(Window oldParent, Window newParent) {} 1873 1874 /++ 1875 Returns the list of the widget's children. 1876 1877 History: 1878 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1879 1880 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1881 +/ 1882 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1883 1884 /++ 1885 Returns the widget's parent. 1886 1887 History: 1888 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1889 1890 The parent should only be managed by the [addChild] and [removeWidget] method. 1891 +/ 1892 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1893 1894 /// The widget's current size. 1895 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1896 /// ditto 1897 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1898 1899 /// Only the layout manager should be calling these. 1900 final protected @property int width(int a) @safe { return _width = a; } 1901 /// ditto 1902 final protected @property int height(int a) @safe { return _height = a; } 1903 1904 /++ 1905 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. 1906 1907 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1908 +/ 1909 protected void registerMovement() { 1910 version(win32_widgets) { 1911 if(hwnd) { 1912 auto pos = getChildPositionRelativeToParentHwnd(this); 1913 MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 1914 this.redraw(); 1915 } 1916 } 1917 sendResizeEvent(); 1918 } 1919 1920 /// Creates the widget and adds it to the parent. 1921 this(Widget parent) { 1922 if(parent !is null) 1923 parent.addChild(this); 1924 setupDefaultEventHandlers(); 1925 } 1926 1927 /// 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. 1928 @scriptable 1929 bool isFocused() { 1930 return parentWindow && parentWindow.focusedWidget is this; 1931 } 1932 1933 private bool showing_ = true; 1934 /// 1935 bool showing() const { return showing_; } 1936 /// 1937 bool hidden() const { return !showing_; } 1938 /++ 1939 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. 1940 1941 Note that a widget only ever shows if all its parents are showing too. 1942 +/ 1943 void showing(bool s, bool recalculate = true) { 1944 if(s != showing_) { 1945 showing_ = s; 1946 // writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s); 1947 1948 showNativeWindowChildren(s); 1949 1950 if(parent && recalculate) { 1951 parent.queueRecomputeChildLayout(); 1952 parent.redraw(); 1953 } 1954 1955 if(s) { 1956 queueRecomputeChildLayout(); 1957 redraw(); 1958 } 1959 } 1960 } 1961 /// Convenience method for `showing = true` 1962 @scriptable 1963 void show() { 1964 showing = true; 1965 } 1966 /// Convenience method for `showing = false` 1967 @scriptable 1968 void hide() { 1969 showing = false; 1970 } 1971 1972 /++ 1973 If you are a native window, show/hide it based on shouldShow and return `true`. 1974 1975 Otherwise, do nothing and return false. 1976 +/ 1977 protected bool showOrHideIfNativeWindow(bool shouldShow) { 1978 version(win32_widgets) { 1979 if(hwnd) { 1980 ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE); 1981 return true; 1982 } else { 1983 return false; 1984 } 1985 } else { 1986 return false; 1987 } 1988 } 1989 1990 private void showNativeWindowChildren(bool s) { 1991 if(!showOrHideIfNativeWindow(s && showing)) 1992 foreach(child; children) 1993 child.showNativeWindowChildren(s); 1994 } 1995 1996 /// 1997 @scriptable 1998 void focus() { 1999 assert(parentWindow !is null); 2000 if(isFocused()) 2001 return; 2002 2003 if(parentWindow.focusedWidget) { 2004 // FIXME: more details here? like from and to 2005 auto from = parentWindow.focusedWidget; 2006 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 2007 parentWindow.focusedWidget = null; 2008 from.emit!BlurEvent(); 2009 from.emit!FocusOutEvent(); 2010 } 2011 2012 2013 version(win32_widgets) { 2014 if(this.hwnd !is null) 2015 SetFocus(this.hwnd); 2016 } 2017 //else static if(UsingSimpledisplayX11) 2018 //this.parentWindow.win.focus(); 2019 2020 parentWindow.focusedWidget = this; 2021 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 2022 this.emit!FocusEvent(); 2023 this.emit!FocusInEvent(); 2024 } 2025 2026 /+ 2027 /++ 2028 Unfocuses the widget. This may reset 2029 +/ 2030 @scriptable 2031 void blur() { 2032 2033 } 2034 +/ 2035 2036 2037 /++ 2038 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 2039 2040 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 2041 +/ 2042 void attachedToWindow(Window w) {} 2043 /++ 2044 Callback when the widget is added to another widget. 2045 2046 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 2047 +/ 2048 void addedTo(Widget w) {} 2049 2050 /++ 2051 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. 2052 2053 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 2054 +/ 2055 protected void addChild(Widget w, int position = int.max) { 2056 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 2057 assert(w !is this, "Child cannot be its own parent!"); 2058 w._parent = this; 2059 if(position == int.max || position == children.length) { 2060 _children ~= w; 2061 } else { 2062 assert(position < _children.length); 2063 _children.length = _children.length + 1; 2064 for(int i = cast(int) _children.length - 1; i > position; i--) 2065 _children[i] = _children[i - 1]; 2066 _children[position] = w; 2067 } 2068 2069 this.parentWindow = this._parentWindow; 2070 2071 w.addedTo(this); 2072 2073 bool parentIsNative; 2074 version(win32_widgets) { 2075 parentIsNative = hwnd !is null; 2076 } 2077 if(!parentIsNative && !showing) 2078 w.showOrHideIfNativeWindow(false); 2079 2080 if(parentWindow !is null) { 2081 w.attachedToWindow(parentWindow); 2082 parentWindow.queueRecomputeChildLayout(); 2083 parentWindow.redraw(); 2084 } 2085 } 2086 2087 /++ 2088 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. 2089 +/ 2090 Widget getChildAtPosition(int x, int y) { 2091 // it goes backward so the last one to show gets picked first 2092 // might use z-index later 2093 foreach_reverse(child; children) { 2094 if(child.hidden) 2095 continue; 2096 if(child.x <= x && child.y <= y 2097 && ((x - child.x) < child.width) 2098 && ((y - child.y) < child.height)) 2099 { 2100 return child; 2101 } 2102 } 2103 2104 return null; 2105 } 2106 2107 /++ 2108 If the widget is a scrollable container, this should add the current scroll position to the given coordinates so the mouse events can be dispatched correctly. 2109 2110 History: 2111 Added July 2, 2021 (v10.2) 2112 +/ 2113 protected void addScrollPosition(ref int x, ref int y) {}; 2114 2115 /++ 2116 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. 2117 2118 This function paints the entire widget, including styled borders, backgrounds, etc. You are also responsible for displaying any important active state to the user, including if you hold the active keyboard focus. If you only want to be responsible for the content while letting the style engine draw the rest, override [paintContent] instead. 2119 2120 [paint] is not called for system widgets as the OS library draws them instead. 2121 2122 2123 The default implementation forwards to [WidgetPainter.drawThemed], passing [paintContent] as the delegate. If you override this, you might use those same functions or you can do your own thing. 2124 2125 You should also look at [WidgetPainter.visualTheme] to be theme aware. 2126 2127 History: 2128 Prior to May 15, 2021, the default implementation was empty. Now, it is `painter.drawThemed(&paintContent);`. You may wish to override [paintContent] instead of [paint] to take advantage of the new styling engine. 2129 +/ 2130 void paint(WidgetPainter painter) { 2131 version(win32_widgets) 2132 if(hwnd) { 2133 return; 2134 } 2135 painter.drawThemed(&paintContent); // note this refers to the following overload 2136 } 2137 2138 /++ 2139 Responsible for drawing the content as the theme engine is responsible for other elements. 2140 2141 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 2142 2143 Params: 2144 painter = your painter (forwarded from [paint]) for drawing on the widget. The clip rectangle and coordinate translation are prepared for you ahead of time so you can use widget coordinates. It also has the theme foreground preloaded into the painter outline color, the theme font preloaded as the painter's active font, and the theme background preloaded as the painter's fill color. 2145 2146 bounds = the bounds, inside the widget, where your content should be drawn. This is the rectangle inside the border and padding (if any). The stuff outside is not clipped - it is still part of your widget - but you should respect these bounds for visual consistency and respecting the theme's area. 2147 2148 If you do want to clip it, you can of course call `auto oldClip = painter.setClipRectangle(bounds); scope(exit) painter.setClipRectangle(oldClip);` to modify it and return to the previous setting when you return. 2149 2150 Returns: 2151 The rectangle representing your actual content. Typically, this is simply `return bounds;`. The theme engine uses this return value to determine where the outline and overlay should be. 2152 2153 History: 2154 Added May 15, 2021 2155 +/ 2156 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2157 return bounds; 2158 } 2159 2160 deprecated("Change ScreenPainter to WidgetPainter") 2161 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 2162 2163 /// I don't actually like the name of this 2164 /// this draws a background on it 2165 void erase(WidgetPainter painter) { 2166 version(win32_widgets) 2167 if(hwnd) return; // Windows will do it. I think. 2168 2169 auto c = getComputedStyle().background.color; 2170 painter.fillColor = c; 2171 painter.outlineColor = c; 2172 2173 version(win32_widgets) { 2174 HANDLE b, p; 2175 if(c.a == 0 && parent is parentWindow) { 2176 // I don't remember why I had this really... 2177 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 2178 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 2179 } 2180 } 2181 painter.drawRectangle(Point(0, 0), width, height); 2182 version(win32_widgets) { 2183 if(c.a == 0 && parent is parentWindow) { 2184 SelectObject(painter.impl.hdc, p); 2185 SelectObject(painter.impl.hdc, b); 2186 } 2187 } 2188 } 2189 2190 /// 2191 WidgetPainter draw() { 2192 int x = this.x, y = this.y; 2193 auto parent = this.parent; 2194 while(parent) { 2195 x += parent.x; 2196 y += parent.y; 2197 parent = parent.parent; 2198 } 2199 2200 auto painter = parentWindow.win.draw(true); 2201 painter.originX = x; 2202 painter.originY = y; 2203 painter.setClipRectangle(Point(0, 0), width, height); 2204 return WidgetPainter(painter, this); 2205 } 2206 2207 /// This can be overridden by scroll things. It is responsible for actually calling [paint]. Do not override unless you've studied minigui.d's source code. There are no stability guarantees if you do override this; it can (and likely will) break without notice. 2208 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 2209 if(hidden) 2210 return; 2211 2212 int paintX = x; 2213 int paintY = y; 2214 if(this.useNativeDrawing()) { 2215 paintX = 0; 2216 paintY = 0; 2217 lox = 0; 2218 loy = 0; 2219 containment = Rectangle(0, 0, int.max, int.max); 2220 } 2221 2222 painter.originX = lox + paintX; 2223 painter.originY = loy + paintY; 2224 2225 bool actuallyPainted = false; 2226 2227 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 2228 if(clip == Rectangle.init) { 2229 // writeln(this, " clipped out"); 2230 return; 2231 } 2232 2233 bool invalidateChildren = invalidate; 2234 2235 if(redrawRequested || force) { 2236 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 2237 2238 painter.drawingUpon = this; 2239 2240 erase(painter); 2241 if(painter.visualTheme) 2242 painter.visualTheme.doPaint(this, painter); 2243 else 2244 paint(painter); 2245 2246 if(invalidate) { 2247 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 2248 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 2249 painter.invalidateRect(region); 2250 // children are contained inside this, so no need to do extra work 2251 invalidateChildren = false; 2252 } 2253 2254 redrawRequested = false; 2255 actuallyPainted = true; 2256 } 2257 2258 foreach(child; children) { 2259 version(win32_widgets) 2260 if(child.useNativeDrawing()) continue; 2261 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 2262 } 2263 2264 version(win32_widgets) 2265 foreach(child; children) { 2266 if(child.useNativeDrawing) { 2267 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 2268 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, true); // have to reset the invalidate flag since these are not necessarily affected the same way, being native children with a clip 2269 } 2270 } 2271 } 2272 2273 protected bool useNativeDrawing() nothrow { 2274 version(win32_widgets) 2275 return hwnd !is null; 2276 else 2277 return false; 2278 } 2279 2280 private static class RedrawEvent {} 2281 private __gshared re = new RedrawEvent(); 2282 2283 private bool redrawRequested; 2284 /// 2285 final void redraw(string file = __FILE__, size_t line = __LINE__) { 2286 redrawRequested = true; 2287 2288 if(this.parentWindow) { 2289 auto sw = this.parentWindow.win; 2290 assert(sw !is null); 2291 if(!sw.eventQueued!RedrawEvent) { 2292 sw.postEvent(re); 2293 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 2294 } 2295 } 2296 } 2297 2298 private SimpleWindow drawableWindow; 2299 2300 /++ 2301 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. 2302 2303 Returns: 2304 `true` if you should do your default behavior. 2305 2306 History: 2307 Added May 5, 2021 2308 2309 Bugs: 2310 It does not do the static checks on gdc right now. 2311 +/ 2312 final protected bool emit(EventType, this This, Args...)(Args args) { 2313 version(GNU) {} else 2314 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2315 auto e = new EventType(this, args); 2316 e.dispatch(); 2317 return !e.defaultPrevented; 2318 } 2319 /// ditto 2320 final protected bool emit(string eventString, this This)() { 2321 auto e = new Event(eventString, this); 2322 e.dispatch(); 2323 return !e.defaultPrevented; 2324 } 2325 2326 /++ 2327 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. 2328 2329 History: 2330 Added May 5, 2021 2331 +/ 2332 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 2333 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2334 return addEventListener(handler); 2335 } 2336 2337 /++ 2338 Gets the computed style properties from the visual theme. 2339 2340 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].) 2341 2342 History: 2343 Added May 8, 2021 2344 +/ 2345 final StyleInformation getComputedStyle() { 2346 return StyleInformation(this); 2347 } 2348 2349 int focusableWidgets(scope int delegate(Widget) dg) { 2350 foreach(widget; WidgetStream(this)) { 2351 if(widget.tabStop && !widget.hidden) { 2352 int result = dg(widget); 2353 if (result) 2354 return result; 2355 } 2356 } 2357 return 0; 2358 } 2359 2360 /++ 2361 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2362 for the given content box (the area between the padding) 2363 2364 History: 2365 Added January 4, 2023 (dub v11.0) 2366 +/ 2367 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2368 auto cs = getComputedStyle(); 2369 2370 auto borderWidth = getBorderWidth(cs.borderStyle); 2371 2372 auto rect = contentBox; 2373 2374 rect.left -= borderWidth; 2375 rect.right += borderWidth; 2376 rect.top -= borderWidth; 2377 rect.bottom += borderWidth; 2378 2379 auto insideBorderRect = rect; 2380 2381 rect.left -= cs.paddingLeft; 2382 rect.right += cs.paddingRight; 2383 rect.top -= cs.paddingTop; 2384 rect.bottom += cs.paddingBottom; 2385 2386 return rect; 2387 } 2388 2389 2390 // FIXME: I kinda want to hide events from implementation widgets 2391 // so it just catches them all and stops propagation... 2392 // i guess i can do it with a event listener on star. 2393 2394 mixin Emits!KeyDownEvent; /// 2395 mixin Emits!KeyUpEvent; /// 2396 mixin Emits!CharEvent; /// 2397 2398 mixin Emits!MouseDownEvent; /// 2399 mixin Emits!MouseUpEvent; /// 2400 mixin Emits!ClickEvent; /// 2401 mixin Emits!DoubleClickEvent; /// 2402 mixin Emits!MouseMoveEvent; /// 2403 mixin Emits!MouseOverEvent; /// 2404 mixin Emits!MouseOutEvent; /// 2405 mixin Emits!MouseEnterEvent; /// 2406 mixin Emits!MouseLeaveEvent; /// 2407 2408 mixin Emits!ResizeEvent; /// 2409 2410 mixin Emits!BlurEvent; /// 2411 mixin Emits!FocusEvent; /// 2412 2413 mixin Emits!FocusInEvent; /// 2414 mixin Emits!FocusOutEvent; /// 2415 } 2416 2417 /+ 2418 /++ 2419 Interface to indicate that the widget has a simple value property. 2420 2421 History: 2422 Added August 26, 2021 2423 +/ 2424 interface HasValue!T { 2425 /// Getter 2426 @property T value(); 2427 /// Setter 2428 @property void value(T); 2429 } 2430 2431 /++ 2432 Interface to indicate that the widget has a range of possible values for its simple value property. 2433 This would be present on something like a slider or possibly a number picker. 2434 2435 History: 2436 Added September 11, 2021 2437 +/ 2438 interface HasRangeOfValues!T : HasValue!T { 2439 /// The minimum and maximum values in the range, inclusive. 2440 @property T minValue(); 2441 @property void minValue(T); /// ditto 2442 @property T maxValue(); /// ditto 2443 @property void maxValue(T); /// ditto 2444 2445 /// The smallest step the user interface allows. User may still type in values without this limitation. 2446 @property void step(T); 2447 @property T step(); /// ditto 2448 } 2449 2450 /++ 2451 Interface to indicate that the widget has a list of possible values the user can choose from. 2452 This would be present on something like a drop-down selector. 2453 2454 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2455 combobox. 2456 2457 History: 2458 Added September 11, 2021 2459 +/ 2460 interface HasListOfValues!T : HasValue!T { 2461 @property T[] values; 2462 @property void values(T[]); 2463 2464 @property int selectedIndex(); // note it may return -1! 2465 @property void selectedIndex(int); 2466 } 2467 +/ 2468 2469 /++ 2470 History: 2471 Added September 2021 (dub v10.4) 2472 +/ 2473 class GridLayout : Layout { 2474 2475 // FIXME: grid padding around edges and also cell spacing between units. even though you could do that by just specifying some gutter yourself in the layout. 2476 2477 /++ 2478 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2479 +/ 2480 enum Gravity { 2481 Center = 0, 2482 NorthWest = North | West, 2483 North = 0b10_00, 2484 NorthEast = North | East, 2485 West = 0b00_10, 2486 East = 0b00_01, 2487 SouthWest = South | West, 2488 South = 0b01_00, 2489 SouthEast = South | East, 2490 } 2491 2492 /++ 2493 The width and height are in some proportional units and can often just be 12. 2494 +/ 2495 this(int width, int height, Widget parent) { 2496 this.gridWidth = width; 2497 this.gridHeight = height; 2498 super(parent); 2499 } 2500 2501 /++ 2502 Sets the position of the given child. 2503 2504 The units of these arguments are in the proportional grid units you set in the constructor. 2505 +/ 2506 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2507 // ensure it is in bounds 2508 // then ensure no overlaps 2509 2510 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2511 2512 foreach(ref position; positions) { 2513 if(position.widget is child) { 2514 position = p; 2515 goto set; 2516 } 2517 } 2518 2519 positions ~= p; 2520 2521 set: 2522 2523 // FIXME: should this batch? 2524 queueRecomputeChildLayout(); 2525 2526 return child; 2527 } 2528 2529 override void addChild(Widget w, int position = int.max) { 2530 super.addChild(w, position); 2531 //positions ~= ChildPosition(w); 2532 if(position != int.max) { 2533 // FIXME: align it so they actually match. 2534 } 2535 } 2536 2537 override void widgetRemoved(size_t idx, Widget w) { 2538 // FIXME: keep the positions array aligned 2539 // positions[idx].widget = null; 2540 } 2541 2542 override void recomputeChildLayout() { 2543 registerMovement(); 2544 int onGrid = cast(int) positions.length; 2545 c: foreach(child; children) { 2546 // just snap it to the grid 2547 if(onGrid) 2548 foreach(position; positions) 2549 if(position.widget is child) { 2550 child.x = this.width * position.x / this.gridWidth; 2551 child.y = this.height * position.y / this.gridHeight; 2552 child.width = this.width * position.width / this.gridWidth; 2553 child.height = this.height * position.height / this.gridHeight; 2554 2555 auto diff = child.width - child.maxWidth(); 2556 // FIXME: gravity? 2557 if(diff > 0) { 2558 child.width = child.width - diff; 2559 2560 if(position.gravity & Gravity.West) { 2561 // nothing needed, already aligned 2562 } else if(position.gravity & Gravity.East) { 2563 child.x += diff; 2564 } else { 2565 child.x += diff / 2; 2566 } 2567 } 2568 2569 diff = child.height - child.maxHeight(); 2570 // FIXME: gravity? 2571 if(diff > 0) { 2572 child.height = child.height - diff; 2573 2574 if(position.gravity & Gravity.North) { 2575 // nothing needed, already aligned 2576 } else if(position.gravity & Gravity.South) { 2577 child.y += diff; 2578 } else { 2579 child.y += diff / 2; 2580 } 2581 } 2582 child.recomputeChildLayout(); 2583 onGrid--; 2584 continue c; 2585 } 2586 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2587 } 2588 } 2589 2590 private struct ChildPosition { 2591 Widget widget; 2592 int x; 2593 int y; 2594 int width; 2595 int height; 2596 Gravity gravity; 2597 } 2598 private ChildPosition[] positions; 2599 2600 int gridWidth = 12; 2601 int gridHeight = 12; 2602 } 2603 2604 /// 2605 abstract class ComboboxBase : Widget { 2606 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2607 // or to always show the list, we want CBS_SIMPLE == 1 2608 version(win32_widgets) 2609 this(uint style, Widget parent) { 2610 super(parent); 2611 createWin32Window(this, "ComboBox"w, null, style); 2612 } 2613 else version(custom_widgets) 2614 this(Widget parent) { 2615 super(parent); 2616 2617 addEventListener((KeyDownEvent event) { 2618 if(event.key == Key.Up) { 2619 setSelection(selection_-1); 2620 event.preventDefault(); 2621 } 2622 if(event.key == Key.Down) { 2623 setSelection(selection_+1); 2624 event.preventDefault(); 2625 } 2626 2627 }); 2628 2629 } 2630 else static assert(false); 2631 2632 protected void scrollSelectionIntoView() {} 2633 2634 /++ 2635 Returns the current list of options in the selection. 2636 2637 History: 2638 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2639 +/ 2640 final @property string[] options() const { 2641 return cast(string[]) options_; 2642 } 2643 2644 /++ 2645 Replaces the list of options in the box. Note that calling this will also reset the selection. 2646 2647 History: 2648 Added December, 29 2024 2649 +/ 2650 final @property void options(string[] options) { 2651 version(win32_widgets) 2652 SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0); 2653 selection_ = -1; 2654 options_ = null; 2655 foreach(opt; options) 2656 addOption(opt); 2657 2658 version(custom_widgets) 2659 redraw(); 2660 } 2661 2662 private string[] options_; 2663 private int selection_ = -1; 2664 2665 /++ 2666 Adds an option to the end of options array. 2667 +/ 2668 void addOption(string s) { 2669 options_ ~= s; 2670 version(win32_widgets) 2671 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2672 } 2673 2674 /++ 2675 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2676 +/ 2677 int getSelection() { 2678 return selection_; 2679 } 2680 2681 /++ 2682 Returns the current selection as a string. 2683 2684 History: 2685 Added November 17, 2021 2686 +/ 2687 string getSelectionString() { 2688 return selection_ == -1 ? null : options[selection_]; 2689 } 2690 2691 /++ 2692 Sets the current selection to an index in the options array, or to the given option if present. 2693 Please note that the string version may do a linear lookup. 2694 2695 Returns: 2696 the index you passed in 2697 2698 History: 2699 The `string` based overload was added on March 1, 2022 (dub v10.7). 2700 2701 The return value was `void` prior to March 1, 2022. 2702 +/ 2703 int setSelection(int idx) { 2704 if(idx < -1) 2705 idx = -1; 2706 if(idx + 1 > options.length) 2707 idx = cast(int) options.length - 1; 2708 2709 selection_ = idx; 2710 2711 version(win32_widgets) 2712 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2713 2714 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2715 t.dispatch(); 2716 2717 scrollSelectionIntoView(); 2718 2719 return idx; 2720 } 2721 2722 /// ditto 2723 int setSelection(string s) { 2724 if(s !is null) 2725 foreach(idx, item; options) 2726 if(item == s) { 2727 return setSelection(cast(int) idx); 2728 } 2729 return setSelection(-1); 2730 } 2731 2732 /++ 2733 This event is fired when the selection changes. Note it inherits 2734 from ChangeEvent!string, meaning you can use that as well, and it also 2735 fills in [Event.intValue]. 2736 +/ 2737 static class SelectionChangedEvent : ChangeEvent!string { 2738 this(Widget target, int iv, string sv) { 2739 super(target, &stringValue); 2740 this.iv = iv; 2741 this.sv = sv; 2742 } 2743 immutable int iv; 2744 immutable string sv; 2745 2746 override @property string stringValue() { return sv; } 2747 override @property int intValue() { return iv; } 2748 } 2749 2750 version(win32_widgets) 2751 override void handleWmCommand(ushort cmd, ushort id) { 2752 if(cmd == CBN_SELCHANGE) { 2753 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2754 fireChangeEvent(); 2755 } 2756 } 2757 2758 private void fireChangeEvent() { 2759 if(selection_ >= options.length) 2760 selection_ = -1; 2761 2762 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2763 t.dispatch(); 2764 } 2765 2766 override int minWidth() { return scaleWithDpi(32); } 2767 2768 version(win32_widgets) { 2769 override int minHeight() { return defaultLineHeight + 6; } 2770 override int maxHeight() { return defaultLineHeight + 6; } 2771 } else { 2772 override int minHeight() { return defaultLineHeight + 4; } 2773 override int maxHeight() { return defaultLineHeight + 4; } 2774 } 2775 2776 version(custom_widgets) 2777 void popup() { 2778 CustomComboBoxPopup popup = new CustomComboBoxPopup(this); 2779 } 2780 2781 } 2782 2783 private class CustomComboBoxPopup : Window { 2784 private ComboboxBase associatedWidget; 2785 private ListWidget lw; 2786 private bool cancelled; 2787 2788 this(ComboboxBase associatedWidget) { 2789 this.associatedWidget = associatedWidget; 2790 2791 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2792 2793 auto w = associatedWidget.width; 2794 // FIXME: suggestedDropdownHeight see below 2795 auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8); 2796 2797 // FIXME: this sux 2798 if(h > associatedWidget.parentWindow.height) 2799 h = associatedWidget.parentWindow.height; 2800 2801 auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok 2802 if(h < mh) 2803 h = mh; 2804 2805 auto coord = associatedWidget.globalCoordinates(); 2806 auto dropDown = new SimpleWindow( 2807 w, h, 2808 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null); 2809 2810 super(dropDown); 2811 2812 dropDown.move(coord.x, coord.y + associatedWidget.height); 2813 2814 this.lw = new ListWidget(this); 2815 version(custom_widgets) 2816 lw.multiSelect = false; 2817 foreach(option; associatedWidget.options) 2818 lw.addOption(option); 2819 2820 auto originalSelection = associatedWidget.getSelection; 2821 lw.setSelection(originalSelection); 2822 lw.scrollSelectionIntoView(); 2823 2824 /+ 2825 { 2826 auto cs = getComputedStyle(); 2827 auto painter = dropDown.draw(); 2828 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2829 auto p = Point(4, 4); 2830 painter.outlineColor = cs.foregroundColor; 2831 foreach(option; associatedWidget.options) { 2832 painter.drawText(p, option); 2833 p.y += defaultLineHeight; 2834 } 2835 } 2836 2837 dropDown.setEventHandlers( 2838 (MouseEvent event) { 2839 if(event.type == MouseEventType.buttonReleased) { 2840 dropDown.close(); 2841 auto element = (event.y - 4) / defaultLineHeight; 2842 if(element >= 0 && element <= associatedWidget.options.length) { 2843 associatedWidget.selection_ = element; 2844 2845 associatedWidget.fireChangeEvent(); 2846 } 2847 } 2848 } 2849 ); 2850 +/ 2851 2852 Widget previouslyFocusedWidget; 2853 2854 dropDown.visibilityChanged = (bool visible) { 2855 if(visible) { 2856 this.redraw(); 2857 captureMouse(this); 2858 //dropDown.grabInput(); 2859 2860 if(previouslyFocusedWidget is null) 2861 previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget; 2862 associatedWidget.parentWindow.focusedWidget = lw; 2863 } else { 2864 //dropDown.releaseInputGrab(); 2865 releaseMouseCapture(); 2866 2867 if(!cancelled) 2868 associatedWidget.setSelection(lw.getSelection); 2869 2870 associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget; 2871 } 2872 }; 2873 2874 dropDown.show(); 2875 } 2876 2877 private bool shouldCloseIfClicked(Widget w) { 2878 if(w is this) 2879 return true; 2880 version(custom_widgets) 2881 if(cast(TextListViewWidget.TextListViewItem) w) 2882 return true; 2883 return false; 2884 } 2885 2886 override void defaultEventHandler_click(ClickEvent ce) { 2887 if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) { 2888 this.win.close(); 2889 } 2890 } 2891 2892 override void defaultEventHandler_char(CharEvent ce) { 2893 if(ce.character == '\n') 2894 this.win.close(); 2895 } 2896 2897 override void defaultEventHandler_keydown(KeyDownEvent kde) { 2898 if(kde.key == Key.Escape) { 2899 cancelled = true; 2900 this.win.close(); 2901 }/+ else if(kde.key == Key.Up || kde.key == Key.Down) 2902 {} // intentionally blank, the list view handles these 2903 // separately from the scroll message widget default handler 2904 else if(lw && lw.glvw && lw.glvw.smw) 2905 lw.glvw.smw.defaultKeyboardListener(kde);+/ 2906 } 2907 } 2908 2909 /++ 2910 A drop-down list where the user must select one of the 2911 given options. Like `<select>` in HTML. 2912 2913 The current selection is given as a string or an index. 2914 It emits a SelectionChangedEvent when it changes. 2915 +/ 2916 class DropDownSelection : ComboboxBase { 2917 /++ 2918 Creates a drop down selection, optionally passing its initial list of options. 2919 2920 History: 2921 The overload with the `options` parameter was added December 29, 2024. 2922 +/ 2923 this(Widget parent) { 2924 version(win32_widgets) 2925 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2926 else version(custom_widgets) { 2927 super(parent); 2928 2929 addEventListener("focus", () { this.redraw; }); 2930 addEventListener("blur", () { this.redraw; }); 2931 addEventListener(EventType.change, () { this.redraw; }); 2932 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2933 addEventListener((KeyDownEvent event) { 2934 if(event.key == Key.Space) 2935 popup(); 2936 }); 2937 } else static assert(false); 2938 } 2939 2940 /// ditto 2941 this(string[] options, Widget parent) { 2942 this(parent); 2943 this.options = options; 2944 } 2945 2946 mixin Padding!q{2}; 2947 static class Style : Widget.Style { 2948 override FrameStyle borderStyle() { return FrameStyle.risen; } 2949 } 2950 mixin OverrideStyle!Style; 2951 2952 version(custom_widgets) 2953 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2954 auto cs = getComputedStyle(); 2955 2956 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2957 2958 painter.outlineColor = cs.foregroundColor; 2959 painter.fillColor = cs.foregroundColor; 2960 2961 /+ 2962 Point[4] triangle; 2963 enum padding = 6; 2964 enum paddingV = 7; 2965 enum triangleWidth = 10; 2966 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2967 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2968 triangle[2] = Point(width - padding - 0, paddingV); 2969 triangle[3] = triangle[0]; 2970 painter.drawPolygon(triangle[]); 2971 +/ 2972 2973 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2974 2975 painter.drawPolygon( 2976 scaleWithDpi(Point(2, 6) + offset), 2977 scaleWithDpi(Point(7, 11) + offset), 2978 scaleWithDpi(Point(12, 6) + offset), 2979 scaleWithDpi(Point(2, 6) + offset) 2980 ); 2981 2982 2983 return bounds; 2984 } 2985 2986 version(win32_widgets) 2987 override void registerMovement() { 2988 version(win32_widgets) { 2989 if(hwnd) { 2990 auto pos = getChildPositionRelativeToParentHwnd(this); 2991 // the height given to this from Windows' perspective is supposed 2992 // to include the drop down's height. so I add to it to give some 2993 // room for that. 2994 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2995 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2996 } 2997 } 2998 sendResizeEvent(); 2999 } 3000 } 3001 3002 /++ 3003 A text box with a drop down arrow listing selections. 3004 The user can choose from the list, or type their own. 3005 +/ 3006 class FreeEntrySelection : ComboboxBase { 3007 this(Widget parent) { 3008 this(null, parent); 3009 } 3010 3011 this(string[] options, Widget parent) { 3012 version(win32_widgets) 3013 super(2 /* CBS_DROPDOWN */, parent); 3014 else version(custom_widgets) { 3015 super(parent); 3016 auto hl = new HorizontalLayout(this); 3017 lineEdit = new LineEdit(hl); 3018 3019 tabStop = false; 3020 3021 // lineEdit.addEventListener((FocusEvent fe) { lineEdit.selectAll(); } ); 3022 3023 auto btn = new class ArrowButton { 3024 this() { 3025 super(ArrowDirection.down, hl); 3026 } 3027 override int heightStretchiness() { 3028 return 1; 3029 } 3030 override int heightShrinkiness() { 3031 return 1; 3032 } 3033 override int maxHeight() { 3034 return lineEdit.maxHeight; 3035 } 3036 }; 3037 //btn.addDirectEventListener("focus", &lineEdit.focus); 3038 btn.addEventListener("triggered", &this.popup); 3039 addEventListener(EventType.change, (Event event) { 3040 lineEdit.content = event.stringValue; 3041 lineEdit.focus(); 3042 redraw(); 3043 }); 3044 } 3045 else static assert(false); 3046 3047 this.options = options; 3048 } 3049 3050 string content() { 3051 version(win32_widgets) 3052 assert(0, "not implemented"); 3053 else version(custom_widgets) 3054 return lineEdit.content; 3055 else static assert(0); 3056 } 3057 3058 void content(string s) { 3059 version(win32_widgets) 3060 assert(0, "not implemented"); 3061 else version(custom_widgets) 3062 lineEdit.content = s; 3063 else static assert(0); 3064 } 3065 3066 version(custom_widgets) { 3067 LineEdit lineEdit; 3068 3069 override int widthStretchiness() { 3070 return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness; 3071 } 3072 override int flexBasisWidth() { 3073 return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth; 3074 } 3075 } 3076 } 3077 3078 /++ 3079 A combination of free entry with a list below it. 3080 +/ 3081 class ComboBox : ComboboxBase { 3082 this(Widget parent) { 3083 version(win32_widgets) 3084 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 3085 else version(custom_widgets) { 3086 super(parent); 3087 lineEdit = new LineEdit(this); 3088 listWidget = new ListWidget(this); 3089 listWidget.multiSelect = false; 3090 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 3091 string c = null; 3092 foreach(option; listWidget.options) 3093 if(option.selected) { 3094 c = option.label; 3095 break; 3096 } 3097 lineEdit.content = c; 3098 }); 3099 3100 listWidget.tabStop = false; 3101 this.tabStop = false; 3102 listWidget.addEventListener("focusin", &lineEdit.focus); 3103 this.addEventListener("focusin", &lineEdit.focus); 3104 3105 addDirectEventListener(EventType.change, { 3106 listWidget.setSelection(selection_); 3107 if(selection_ != -1) 3108 lineEdit.content = options[selection_]; 3109 lineEdit.focus(); 3110 redraw(); 3111 }); 3112 3113 lineEdit.addEventListener("focusin", &lineEdit.selectAll); 3114 3115 listWidget.addDirectEventListener(EventType.change, { 3116 int set = -1; 3117 foreach(idx, opt; listWidget.options) 3118 if(opt.selected) { 3119 set = cast(int) idx; 3120 break; 3121 } 3122 if(set != selection_) 3123 this.setSelection(set); 3124 }); 3125 } else static assert(false); 3126 } 3127 3128 override int minHeight() { return defaultLineHeight * 3; } 3129 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 3130 override int heightStretchiness() { return 5; } 3131 3132 version(custom_widgets) { 3133 LineEdit lineEdit; 3134 ListWidget listWidget; 3135 3136 override void addOption(string s) { 3137 listWidget.addOption(s); 3138 ComboboxBase.addOption(s); 3139 } 3140 3141 override void scrollSelectionIntoView() { 3142 listWidget.scrollSelectionIntoView(); 3143 } 3144 } 3145 } 3146 3147 /+ 3148 class Spinner : Widget { 3149 version(win32_widgets) 3150 this(Widget parent) { 3151 super(parent); 3152 parentWindow = parent.parentWindow; 3153 auto hlayout = new HorizontalLayout(this); 3154 lineEdit = new LineEdit(hlayout); 3155 upDownControl = new UpDownControl(hlayout); 3156 } 3157 3158 LineEdit lineEdit; 3159 UpDownControl upDownControl; 3160 } 3161 3162 class UpDownControl : Widget { 3163 version(win32_widgets) 3164 this(Widget parent) { 3165 super(parent); 3166 parentWindow = parent.parentWindow; 3167 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 3168 } 3169 3170 override int minHeight() { return defaultLineHeight; } 3171 override int maxHeight() { return defaultLineHeight * 3/2; } 3172 3173 override int minWidth() { return defaultLineHeight * 3/2; } 3174 override int maxWidth() { return defaultLineHeight * 3/2; } 3175 } 3176 +/ 3177 3178 /+ 3179 class DataView : Widget { 3180 // this is the omnibus data viewer 3181 // the internal data layout is something like: 3182 // string[string][] but also each node can have parents 3183 } 3184 +/ 3185 3186 3187 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 3188 3189 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 3190 3191 // FIXME: menus should prolly capture the mouse. ugh i kno. 3192 /* 3193 TextEdit needs: 3194 3195 * caret manipulation 3196 * selection control 3197 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 3198 3199 For example: 3200 3201 connect(paste, &textEdit.insertTextAtCaret); 3202 3203 would be nice. 3204 3205 3206 3207 I kinda want an omnibus dataview that combines list, tree, 3208 and table - it can be switched dynamically between them. 3209 3210 Flattening policy: only show top level, show recursive, show grouped 3211 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 3212 3213 Single select, multi select, organization, drag+drop 3214 */ 3215 3216 //static if(UsingSimpledisplayX11) 3217 version(win32_widgets) {} 3218 else version(custom_widgets) { 3219 enum scrollClickRepeatInterval = 50; 3220 3221 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 3222 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 3223 enum activeTabColor = lightAccentColor; 3224 enum hoveringColor = Color(228, 228, 228); 3225 enum buttonColor = windowBackgroundColor; 3226 enum depressedButtonColor = darkAccentColor; 3227 enum activeListXorColor = Color(255, 255, 127); 3228 enum progressBarColor = Color(0, 0, 128); 3229 enum activeMenuItemColor = Color(0, 0, 128); 3230 3231 }} 3232 else static assert(false); 3233 deprecated("Get these properties off the `visualTheme` instead.") { 3234 // these are used by horizontal rule so not just custom_widgets. for now at least. 3235 enum darkAccentColor = Color(172, 172, 172); 3236 enum lightAccentColor = Color(223, 223, 223); // used to be 223 3237 } 3238 3239 private const(wchar)* toWstringzInternal(in char[] s) { 3240 wchar[] str; 3241 str.reserve(s.length + 1); 3242 foreach(dchar ch; s) 3243 str ~= ch; 3244 str ~= '\0'; 3245 return str.ptr; 3246 } 3247 3248 static if(SimpledisplayTimerAvailable) 3249 void setClickRepeat(Widget w, int interval, int delay = 250) { 3250 Timer timer; 3251 int delayRemaining = delay / interval; 3252 if(delayRemaining <= 1) 3253 delayRemaining = 2; 3254 3255 immutable originalDelayRemaining = delayRemaining; 3256 3257 w.addDirectEventListener((scope MouseDownEvent ev) { 3258 if(ev.srcElement !is w) 3259 return; 3260 if(timer !is null) { 3261 timer.destroy(); 3262 timer = null; 3263 } 3264 delayRemaining = originalDelayRemaining; 3265 timer = new Timer(interval, () { 3266 if(delayRemaining > 0) 3267 delayRemaining--; 3268 else { 3269 auto ev = new Event("triggered", w); 3270 ev.sendDirectly(); 3271 } 3272 }); 3273 }); 3274 3275 w.addDirectEventListener((scope MouseUpEvent ev) { 3276 if(ev.srcElement !is w) 3277 return; 3278 if(timer !is null) { 3279 timer.destroy(); 3280 timer = null; 3281 } 3282 }); 3283 3284 w.addDirectEventListener((scope MouseLeaveEvent ev) { 3285 if(ev.srcElement !is w) 3286 return; 3287 if(timer !is null) { 3288 timer.destroy(); 3289 timer = null; 3290 } 3291 }); 3292 3293 } 3294 else 3295 void setClickRepeat(Widget w, int interval, int delay = 250) {} 3296 3297 enum FrameStyle { 3298 none, /// 3299 risen, /// a 3d pop-out effect (think Windows 95 button) 3300 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 3301 solid, /// 3302 dotted, /// 3303 fantasy, /// a style based on a popular fantasy video game 3304 rounded, /// a rounded rectangle 3305 } 3306 3307 version(custom_widgets) 3308 deprecated 3309 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 3310 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3311 } 3312 3313 version(custom_widgets) 3314 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 3315 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 3316 } 3317 3318 version(custom_widgets) 3319 deprecated 3320 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 3321 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3322 } 3323 3324 int getBorderWidth(FrameStyle style) { 3325 final switch(style) { 3326 case FrameStyle.sunk, FrameStyle.risen: 3327 return 2; 3328 case FrameStyle.none: 3329 return 0; 3330 case FrameStyle.solid: 3331 return 1; 3332 case FrameStyle.dotted: 3333 return 1; 3334 case FrameStyle.fantasy: 3335 return 3; 3336 case FrameStyle.rounded: 3337 return 2; 3338 } 3339 } 3340 3341 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 3342 int borderWidth = getBorderWidth(style); 3343 final switch(style) { 3344 case FrameStyle.sunk, FrameStyle.risen: 3345 // outer layer 3346 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 3347 break; 3348 case FrameStyle.none: 3349 painter.outlineColor = background; 3350 break; 3351 case FrameStyle.solid: 3352 case FrameStyle.rounded: 3353 painter.pen = Pen(border, 1); 3354 break; 3355 case FrameStyle.dotted: 3356 painter.pen = Pen(border, 1, Pen.Style.Dotted); 3357 break; 3358 case FrameStyle.fantasy: 3359 painter.pen = Pen(border, 3); 3360 break; 3361 } 3362 3363 painter.fillColor = background; 3364 3365 if(style == FrameStyle.rounded) { 3366 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 3367 } else { 3368 painter.drawRectangle(Point(x + 0, y + 0), width, height); 3369 3370 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 3371 // 3d effect 3372 auto vt = WidgetPainter.visualTheme; 3373 3374 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 3375 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 3376 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 3377 3378 // inner layer 3379 //right, bottom 3380 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 3381 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 3382 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 3383 // left, top 3384 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 3385 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 3386 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 3387 } else if(style == FrameStyle.fantasy) { 3388 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 3389 painter.fillColor = Color.transparent; 3390 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 3391 } 3392 } 3393 3394 return borderWidth; 3395 } 3396 3397 /++ 3398 An `Action` represents some kind of user action they can trigger through menu options, toolbars, hotkeys, and similar mechanisms. The text label, icon, and handlers are centrally held here instead of repeated in each UI element. 3399 3400 See_Also: 3401 [MenuItem] 3402 [ToolButton] 3403 [Menu.addItem] 3404 +/ 3405 class Action { 3406 version(win32_widgets) { 3407 private int id; 3408 private static int lastId = 9000; 3409 private static Action[int] mapping; 3410 } 3411 3412 KeyEvent accelerator; 3413 3414 // FIXME: disable message 3415 // and toggle thing? 3416 // ??? and trigger arguments too ??? 3417 3418 /++ 3419 Params: 3420 label = the textual label 3421 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 3422 triggered = initial handler, more can be added via the [triggered] member. 3423 +/ 3424 this(string label, ushort icon = 0, void delegate() triggered = null) { 3425 this.label = label; 3426 this.iconId = icon; 3427 if(triggered !is null) 3428 this.triggered ~= triggered; 3429 version(win32_widgets) { 3430 id = ++lastId; 3431 mapping[id] = this; 3432 } 3433 } 3434 3435 private string label; 3436 private ushort iconId; 3437 // icon 3438 3439 // when it is triggered, the triggered event is fired on the window 3440 /// The list of handlers when it is triggered. 3441 void delegate()[] triggered; 3442 } 3443 3444 /* 3445 plan: 3446 keyboard accelerators 3447 3448 * menus (and popups and tooltips) 3449 * status bar 3450 * toolbars and buttons 3451 3452 sortable table view 3453 3454 maybe notification area icons 3455 basic clipboard 3456 3457 * radio box 3458 splitter 3459 toggle buttons (optionally mutually exclusive, like in Paint) 3460 label, rich text display, multi line plain text (selectable) 3461 * fieldset 3462 * nestable grid layout 3463 single line text input 3464 * multi line text input 3465 slider 3466 spinner 3467 list box 3468 drop down 3469 combo box 3470 auto complete box 3471 * progress bar 3472 3473 terminal window/widget (on unix it might even be a pty but really idk) 3474 3475 ok button 3476 cancel button 3477 3478 keyboard hotkeys 3479 3480 scroll widget 3481 3482 event redirections and network transparency 3483 script integration 3484 */ 3485 3486 3487 /* 3488 MENUS 3489 3490 auto bar = new MenuBar(window); 3491 window.menuBar = bar; 3492 3493 auto fileMenu = bar.addItem(new Menu("&File")); 3494 fileMenu.addItem(new MenuItem("&Exit")); 3495 3496 3497 EVENTS 3498 3499 For controls, you should usually use "triggered" rather than "click", etc., because 3500 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3501 This is the case on menus and pushbuttons. 3502 3503 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3504 */ 3505 3506 3507 /* 3508 enum LinePreference { 3509 AlwaysOnOwnLine, // always on its own line 3510 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3511 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 3512 } 3513 */ 3514 3515 /++ 3516 Convenience mixin for overriding all four sides of margin or padding in a [Widget] with the same code. It mixes in the given string as the return value of the four overridden methods. 3517 3518 --- 3519 class MyWidget : Widget { 3520 this(Widget parent) { super(parent); } 3521 3522 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3523 mixin Padding!q{4}; 3524 3525 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3526 mixin Margin!q{8}; 3527 3528 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3529 // while Top/Bottom/Right remain 8 from the mixin above. 3530 override int marginLeft() { return 2; } 3531 } 3532 --- 3533 3534 3535 The minigui layout model is based on the web's CSS box model. The layout engine* arranges widgets based on their margin for separation and assigns them a size based on thier preferences (e.g. [Widget.minHeight]) and the available space. Widgets are assigned a size by the layout engine. Inside this size, they have a border (see [Widget.Style.borderWidth]), then padding space, and then their content. Their content box may also have an outline drawn on top of it (see [Widget.Style.outlineStyle]). 3536 3537 Padding is the area inside a widget where its background is drawn, but the content avoids. 3538 3539 Margin is the area between widgets. The algorithm is the spacing between any two widgets is the max of their adjacent margins (not the sum!). 3540 3541 * Some widgets do not participate in placement, e.g. [StaticPosition], and some layout systems do their own separate thing too; ultimately, these properties are just hints to the layout function and you can always implement your own to do whatever you want. But this statement is still mostly true. 3542 +/ 3543 mixin template Padding(string code) { 3544 override int paddingLeft() { return mixin(code);} 3545 override int paddingRight() { return mixin(code);} 3546 override int paddingTop() { return mixin(code);} 3547 override int paddingBottom() { return mixin(code);} 3548 } 3549 3550 /// ditto 3551 mixin template Margin(string code) { 3552 override int marginLeft() { return mixin(code);} 3553 override int marginRight() { return mixin(code);} 3554 override int marginTop() { return mixin(code);} 3555 override int marginBottom() { return mixin(code);} 3556 } 3557 3558 private 3559 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3560 enum calcingV = relevantMeasure == "height"; 3561 3562 parent.registerMovement(); 3563 3564 if(parent.children.length == 0) 3565 return; 3566 3567 auto parentStyle = parent.getComputedStyle(); 3568 3569 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3570 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3571 3572 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3573 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3574 3575 // my own width and height should already be set by the caller of this function... 3576 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3577 mixin("parentStyle.padding"~firstThingy~"()") - 3578 mixin("parentStyle.padding"~secondThingy~"()"); 3579 3580 int stretchinessSum; 3581 int stretchyChildSum; 3582 int lastMargin = 0; 3583 3584 int shrinkinessSum; 3585 int shrinkyChildSum; 3586 3587 // set initial size 3588 foreach(child; parent.children) { 3589 3590 auto childStyle = child.getComputedStyle(); 3591 3592 if(cast(StaticPosition) child) 3593 continue; 3594 if(child.hidden) 3595 continue; 3596 3597 const iw = child.flexBasisWidth(); 3598 const ih = child.flexBasisHeight(); 3599 3600 static if(calcingV) { 3601 child.width = parent.width - 3602 mixin("childStyle.margin"~otherFirstThingy~"()") - 3603 mixin("childStyle.margin"~otherSecondThingy~"()") - 3604 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3605 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3606 3607 if(child.width < 0) 3608 child.width = 0; 3609 if(child.width > childStyle.maxWidth()) 3610 child.width = childStyle.maxWidth(); 3611 3612 if(iw > 0) { 3613 auto totalPossible = child.width; 3614 if(child.width > iw && child.widthStretchiness() == 0) 3615 child.width = iw; 3616 } 3617 3618 child.height = mymax(childStyle.minHeight(), ih); 3619 } else { 3620 // set to take all the space 3621 child.height = parent.height - 3622 mixin("childStyle.margin"~firstThingy~"()") - 3623 mixin("childStyle.margin"~secondThingy~"()") - 3624 mixin("parentStyle.padding"~firstThingy~"()") - 3625 mixin("parentStyle.padding"~secondThingy~"()"); 3626 3627 // then clamp it 3628 if(child.height < 0) 3629 child.height = 0; 3630 if(child.height > childStyle.maxHeight()) 3631 child.height = childStyle.maxHeight(); 3632 3633 // and if possible, respect the ideal target 3634 if(ih > 0) { 3635 auto totalPossible = child.height; 3636 if(child.height > ih && child.heightStretchiness() == 0) 3637 child.height = ih; 3638 } 3639 3640 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3641 child.width = mymax(childStyle.minWidth(), iw); 3642 } 3643 3644 spaceRemaining -= mixin("child." ~ relevantMeasure); 3645 3646 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3647 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3648 lastMargin = margin; 3649 spaceRemaining -= thisMargin + margin; 3650 3651 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3652 stretchinessSum += s; 3653 if(s > 0) 3654 stretchyChildSum++; 3655 3656 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3657 shrinkinessSum += s2; 3658 if(s2 > 0) 3659 shrinkyChildSum++; 3660 } 3661 3662 if(spaceRemaining < 0 && shrinkyChildSum) { 3663 // shrink to get into the space if it is possible 3664 auto toRemove = -spaceRemaining; 3665 auto removalPerItem = toRemove / shrinkinessSum; 3666 auto remainder = toRemove % shrinkinessSum; 3667 3668 // FIXME: wtf why am i shrinking things with no shrinkiness? 3669 3670 foreach(child; parent.children) { 3671 auto childStyle = child.getComputedStyle(); 3672 if(cast(StaticPosition) child) 3673 continue; 3674 if(child.hidden) 3675 continue; 3676 static if(calcingV) { 3677 auto minimum = childStyle.minHeight(); 3678 auto stretch = childStyle.heightShrinkiness(); 3679 } else { 3680 auto minimum = childStyle.minWidth(); 3681 auto stretch = childStyle.widthShrinkiness(); 3682 } 3683 3684 if(mixin("child._" ~ relevantMeasure) <= minimum) 3685 continue; 3686 // import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch); 3687 3688 mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh. 3689 3690 spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum; 3691 } 3692 } 3693 3694 // stretch to fill space 3695 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3696 auto spacePerChild = spaceRemaining / stretchinessSum; 3697 bool spreadEvenly; 3698 bool giveToBiggest; 3699 if(spacePerChild <= 0) { 3700 spacePerChild = spaceRemaining / stretchyChildSum; 3701 spreadEvenly = true; 3702 } 3703 if(spacePerChild <= 0) { 3704 giveToBiggest = true; 3705 } 3706 int previousSpaceRemaining = spaceRemaining; 3707 stretchinessSum = 0; 3708 Widget mostStretchy; 3709 int mostStretchyS; 3710 foreach(child; parent.children) { 3711 auto childStyle = child.getComputedStyle(); 3712 if(cast(StaticPosition) child) 3713 continue; 3714 if(child.hidden) 3715 continue; 3716 static if(calcingV) { 3717 auto maximum = childStyle.maxHeight(); 3718 } else { 3719 auto maximum = childStyle.maxWidth(); 3720 } 3721 3722 if(mixin("child." ~ relevantMeasure) >= maximum) { 3723 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3724 mixin("child._" ~ relevantMeasure) -= adj; 3725 spaceRemaining += adj; 3726 continue; 3727 } 3728 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3729 if(s <= 0) 3730 continue; 3731 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3732 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3733 spaceRemaining -= spaceAdjustment; 3734 if(mixin("child." ~ relevantMeasure) > maximum) { 3735 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3736 mixin("child._" ~ relevantMeasure) -= diff; 3737 spaceRemaining += diff; 3738 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3739 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3740 if(mostStretchy is null || s >= mostStretchyS) { 3741 mostStretchy = child; 3742 mostStretchyS = s; 3743 } 3744 } 3745 } 3746 3747 if(giveToBiggest && mostStretchy !is null) { 3748 auto child = mostStretchy; 3749 auto childStyle = child.getComputedStyle(); 3750 int spaceAdjustment = spaceRemaining; 3751 3752 static if(calcingV) 3753 auto maximum = childStyle.maxHeight(); 3754 else 3755 auto maximum = childStyle.maxWidth(); 3756 3757 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3758 spaceRemaining -= spaceAdjustment; 3759 if(mixin("child._" ~ relevantMeasure) > maximum) { 3760 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3761 mixin("child._" ~ relevantMeasure) -= diff; 3762 spaceRemaining += diff; 3763 } 3764 } 3765 3766 if(spaceRemaining == previousSpaceRemaining) { 3767 if(mostStretchy !is null) { 3768 static if(calcingV) 3769 auto maximum = mostStretchy.maxHeight(); 3770 else 3771 auto maximum = mostStretchy.maxWidth(); 3772 3773 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3774 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3775 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3776 } 3777 break; // apparently nothing more we can do 3778 } 3779 } 3780 3781 foreach(child; parent.children) { 3782 auto childStyle = child.getComputedStyle(); 3783 if(cast(StaticPosition) child) 3784 continue; 3785 if(child.hidden) 3786 continue; 3787 3788 static if(calcingV) 3789 auto maximum = childStyle.maxHeight(); 3790 else 3791 auto maximum = childStyle.maxWidth(); 3792 if(mixin("child._" ~ relevantMeasure) > maximum) 3793 mixin("child._" ~ relevantMeasure) = maximum; 3794 } 3795 3796 // position 3797 lastMargin = 0; 3798 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3799 foreach(child; parent.children) { 3800 auto childStyle = child.getComputedStyle(); 3801 if(cast(StaticPosition) child) { 3802 child.recomputeChildLayout(); 3803 continue; 3804 } 3805 if(child.hidden) 3806 continue; 3807 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3808 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3809 currentPos += thisMargin; 3810 static if(calcingV) { 3811 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3812 child.y = currentPos; 3813 } else { 3814 child.x = currentPos; 3815 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3816 3817 } 3818 currentPos += mixin("child." ~ relevantMeasure); 3819 currentPos += margin; 3820 lastMargin = margin; 3821 3822 child.recomputeChildLayout(); 3823 } 3824 } 3825 3826 int mymax(int a, int b) { return a > b ? a : b; } 3827 int mymax(int a, int b, int c) { 3828 auto d = mymax(a, b); 3829 return c > d ? c : d; 3830 } 3831 3832 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3833 // and here, it must be integrable with the layout, the event system, and not be painted over. 3834 version(win32_widgets) { 3835 3836 // this function just does stuff that a parent window needs for redirection 3837 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3838 this_.hookedWndProc(msg, wParam, lParam); 3839 3840 switch(msg) { 3841 3842 case WM_VSCROLL, WM_HSCROLL: 3843 auto pos = HIWORD(wParam); 3844 auto m = LOWORD(wParam); 3845 3846 auto scrollbarHwnd = cast(HWND) lParam; 3847 3848 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3849 3850 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3851 3852 switch(m) { 3853 /+ 3854 // I don't think those messages are ever actually sent normally by the widget itself, 3855 // they are more used for the keyboard interface. methinks. 3856 case SB_BOTTOM: 3857 // writeln("end"); 3858 auto event = new Event("scrolltoend", *widgetp); 3859 event.dispatch(); 3860 //if(!event.defaultPrevented) 3861 break; 3862 case SB_TOP: 3863 // writeln("top"); 3864 auto event = new Event("scrolltobeginning", *widgetp); 3865 event.dispatch(); 3866 break; 3867 case SB_ENDSCROLL: 3868 // idk 3869 break; 3870 +/ 3871 case SB_LINEDOWN: 3872 (*widgetp).emitCommand!"scrolltonextline"(); 3873 return 0; 3874 case SB_LINEUP: 3875 (*widgetp).emitCommand!"scrolltopreviousline"(); 3876 return 0; 3877 case SB_PAGEDOWN: 3878 (*widgetp).emitCommand!"scrolltonextpage"(); 3879 return 0; 3880 case SB_PAGEUP: 3881 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3882 return 0; 3883 case SB_THUMBPOSITION: 3884 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3885 ev.dispatch(); 3886 return 0; 3887 case SB_THUMBTRACK: 3888 // eh kinda lying but i like the real time update display 3889 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3890 ev.dispatch(); 3891 3892 // the event loop doesn't seem to carry on with a requested redraw.. 3893 // so we request it to get our dirty bit set... 3894 // then we need to immediately actually redraw it too for instant feedback to user 3895 SimpleWindow.processAllCustomEvents(); 3896 SimpleWindow.processAllCustomEvents(); 3897 //if(this_.parentWindow) 3898 //this_.parentWindow.actualRedraw(); 3899 3900 // and this ensures the WM_PAINT message is sent fairly quickly 3901 // still seems to lag a little in large windows but meh it basically works. 3902 if(this_.parentWindow) { 3903 // FIXME: if painting is slow, this does still lag 3904 // we probably will want to expose some user hook to ScrollWindowEx 3905 // or something. 3906 UpdateWindow(this_.parentWindow.hwnd); 3907 } 3908 return 0; 3909 default: 3910 } 3911 } 3912 break; 3913 3914 case WM_CONTEXTMENU: 3915 auto hwndFrom = cast(HWND) wParam; 3916 3917 auto xPos = cast(short) LOWORD(lParam); 3918 auto yPos = cast(short) HIWORD(lParam); 3919 3920 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3921 POINT p; 3922 p.x = xPos; 3923 p.y = yPos; 3924 ScreenToClient(hwnd, &p); 3925 auto clientX = cast(ushort) p.x; 3926 auto clientY = cast(ushort) p.y; 3927 3928 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3929 3930 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3931 return 0; 3932 } 3933 } 3934 break; 3935 3936 case WM_DRAWITEM: 3937 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3938 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3939 return (*widgetp).handleWmDrawItem(dis); 3940 } 3941 break; 3942 3943 case WM_NOTIFY: 3944 auto hdr = cast(NMHDR*) lParam; 3945 auto hwndFrom = hdr.hwndFrom; 3946 auto code = hdr.code; 3947 3948 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3949 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3950 } 3951 break; 3952 case WM_COMMAND: 3953 auto handle = cast(HWND) lParam; 3954 auto cmd = HIWORD(wParam); 3955 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3956 3957 default: 3958 // pass it on 3959 } 3960 return 0; 3961 } 3962 3963 3964 3965 extern(Windows) 3966 private 3967 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3968 // but can i merge them?! 3969 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3970 // try { writeln(iMessage); } catch(Exception e) {}; 3971 3972 if(auto te = hWnd in Widget.nativeMapping) { 3973 try { 3974 3975 te.hookedWndProc(iMessage, wParam, lParam); 3976 3977 int mustReturn; 3978 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3979 if(mustReturn) 3980 return ret; 3981 3982 if(iMessage == WM_SETFOCUS) { 3983 auto lol = *te; 3984 while(lol !is null && lol.implicitlyCreated) 3985 lol = lol.parent; 3986 lol.focus(); 3987 //(*te).parentWindow.focusedWidget = lol; 3988 } 3989 3990 3991 if(iMessage == WM_CTLCOLOREDIT) { 3992 3993 } 3994 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3995 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3996 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3997 //GetStockObject(NULL_BRUSH); 3998 } 3999 4000 auto pos = getChildPositionRelativeToParentOrigin(*te); 4001 lastDefaultPrevented = false; 4002 // try { writeln(typeid(*te)); } catch(Exception e) {} 4003 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 4004 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 4005 else { 4006 // it was something we recognized, should only call the window procedure if the default was not prevented 4007 } 4008 } catch(Exception e) { 4009 assert(0, e.toString()); 4010 } 4011 return 0; 4012 } 4013 assert(0, "shouldn't be receiving messages for this window...."); 4014 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 4015 } 4016 4017 extern(Windows) 4018 private 4019 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 4020 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4021 if(iMessage == WM_ERASEBKGND) { 4022 auto dc = GetDC(hWnd); 4023 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 4024 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 4025 RECT r; 4026 GetWindowRect(hWnd, &r); 4027 // since the pen is null, to fill the whole space, we need the +1 on both. 4028 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 4029 SelectObject(dc, p); 4030 SelectObject(dc, b); 4031 ReleaseDC(hWnd, dc); 4032 InvalidateRect(hWnd, null, false); // redraw the border 4033 return 1; 4034 } 4035 return HookedWndProc(hWnd, iMessage, wParam, lParam); 4036 } 4037 4038 /++ 4039 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 4040 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 4041 of minigui's expectations. 4042 4043 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 4044 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 4045 4046 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 4047 4048 To check if you can use this, use `static if(UsingWin32Widgets)`. 4049 +/ 4050 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 4051 assert(p.parentWindow !is null); 4052 assert(p.parentWindow.win.impl.hwnd !is null); 4053 4054 auto bsgroupbox = style == BS_GROUPBOX; 4055 4056 HWND phwnd; 4057 4058 auto wtf = p.parent; 4059 while(wtf) { 4060 if(wtf.hwnd !is null) { 4061 phwnd = wtf.hwnd; 4062 break; 4063 } 4064 wtf = wtf.parent; 4065 } 4066 4067 if(phwnd is null) 4068 phwnd = p.parentWindow.win.impl.hwnd; 4069 4070 assert(phwnd !is null); 4071 4072 WCharzBuffer wt = WCharzBuffer(windowText); 4073 4074 style |= WS_VISIBLE | WS_CHILD; 4075 //if(className != WC_TABCONTROL) 4076 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 4077 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 4078 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 4079 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 4080 4081 assert(p.hwnd !is null); 4082 4083 4084 static HFONT font; 4085 if(font is null) { 4086 NONCLIENTMETRICS params; 4087 params.cbSize = params.sizeof; 4088 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 4089 font = CreateFontIndirect(¶ms.lfMessageFont); 4090 } 4091 } 4092 4093 if(font) 4094 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 4095 4096 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 4097 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 4098 Widget.nativeMapping[p.hwnd] = p; 4099 4100 if(bsgroupbox) 4101 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 4102 else 4103 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4104 4105 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 4106 4107 p.registerMovement(); 4108 } 4109 } 4110 4111 version(win32_widgets) 4112 private 4113 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 4114 if(hwnd is null || hwnd in Widget.nativeMapping) 4115 return true; 4116 auto parent = cast(Widget) cast(void*) lparam; 4117 Widget p = new Widget(null); 4118 p._parent = parent; 4119 p.parentWindow = parent.parentWindow; 4120 p.hwnd = hwnd; 4121 p.implicitlyCreated = true; 4122 Widget.nativeMapping[p.hwnd] = p; 4123 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4124 return true; 4125 } 4126 4127 /++ 4128 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 4129 +/ 4130 struct WidgetPainter { 4131 this(ScreenPainter screenPainter, Widget drawingUpon) { 4132 this.drawingUpon = drawingUpon; 4133 this.screenPainter = screenPainter; 4134 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4135 this.screenPainter.setFont(font); 4136 } 4137 4138 /++ 4139 EXPERIMENTAL. subject to change. 4140 4141 When you draw a cursor, you can draw this to notify your window of where it is, 4142 for IME systems to use. 4143 +/ 4144 void notifyCursorPosition(int x, int y, int width, int height) { 4145 if(auto a = drawingUpon.parentWindow) 4146 if(auto w = a.inputProxy) { 4147 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 4148 } 4149 } 4150 4151 4152 /// 4153 ScreenPainter screenPainter; 4154 /// Forward to the screen painter for other methods 4155 alias screenPainter this; 4156 4157 private Widget drawingUpon; 4158 4159 /++ 4160 This is the list of rectangles that actually need to be redrawn. 4161 4162 Not actually implemented yet. 4163 +/ 4164 Rectangle[] invalidatedRectangles; 4165 4166 private static BaseVisualTheme _visualTheme; 4167 4168 /++ 4169 Functions to access the visual theme and helpers to easily use it. 4170 4171 These are aware of the current widget's computed style out of the theme. 4172 +/ 4173 static @property BaseVisualTheme visualTheme() { 4174 if(_visualTheme is null) 4175 _visualTheme = new DefaultVisualTheme(); 4176 return _visualTheme; 4177 } 4178 4179 /// ditto 4180 static @property void visualTheme(BaseVisualTheme theme) { 4181 _visualTheme = theme; 4182 4183 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 4184 } 4185 4186 /// ditto 4187 Color themeForeground() { 4188 return drawingUpon.getComputedStyle().foregroundColor(); 4189 } 4190 4191 /// ditto 4192 Color themeBackground() { 4193 return drawingUpon.getComputedStyle().background.color; 4194 } 4195 4196 int isDarkTheme() { 4197 return 0; // unspecified, yes, no as enum. FIXME 4198 } 4199 4200 /++ 4201 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. 4202 4203 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 4204 4205 If you change teh clip rectangle, you should change it back before you return. 4206 4207 4208 The sequence it uses is: 4209 background 4210 content (delegated to you) 4211 border 4212 focused outline 4213 selected overlay 4214 4215 Example code: 4216 4217 --- 4218 void paint(WidgetPainter painter) { 4219 painter.drawThemed((bounds) { 4220 return bounds; // if the selection overlay should be contained, you can return it here. 4221 }); 4222 } 4223 --- 4224 +/ 4225 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 4226 drawThemed((WidgetPainter painter, const Rectangle bounds) { 4227 return drawBody(bounds); 4228 }); 4229 } 4230 // this overload is actually mroe for setting the delegate to a virtual function 4231 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 4232 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 4233 4234 auto cs = drawingUpon.getComputedStyle(); 4235 4236 auto bg = cs.background.color; 4237 4238 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 4239 4240 rect.left += borderWidth; 4241 rect.right -= borderWidth; 4242 rect.top += borderWidth; 4243 rect.bottom -= borderWidth; 4244 4245 auto insideBorderRect = rect; 4246 4247 rect.left += cs.paddingLeft; 4248 rect.right -= cs.paddingRight; 4249 rect.top += cs.paddingTop; 4250 rect.bottom -= cs.paddingBottom; 4251 4252 this.outlineColor = this.themeForeground; 4253 this.fillColor = bg; 4254 4255 auto widgetFont = cs.fontCached; 4256 if(widgetFont !is null) 4257 this.setFont(widgetFont); 4258 4259 rect = drawBody(this, rect); 4260 4261 if(widgetFont !is null) { 4262 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4263 this.setFont(vtFont); 4264 else 4265 this.setFont(null); 4266 } 4267 4268 if(auto os = cs.outlineStyle()) { 4269 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 4270 this.fillColor = Color.transparent; 4271 this.drawRectangle(insideBorderRect); 4272 } 4273 } 4274 4275 /++ 4276 First, draw the background. 4277 Then draw your content. 4278 Next, draw the border. 4279 And the focused indicator. 4280 And the is-selected box. 4281 4282 If it is focused i can draw the outline too... 4283 4284 If selected i can even do the xor action but that's at the end. 4285 +/ 4286 void drawThemeBackground() { 4287 4288 } 4289 4290 void drawThemeBorder() { 4291 4292 } 4293 4294 // all this stuff is a dangerous experiment.... 4295 static class ScriptableVersion { 4296 ScreenPainterImplementation* p; 4297 int originX, originY; 4298 4299 @scriptable: 4300 void drawRectangle(int x, int y, int width, int height) { 4301 p.drawRectangle(x + originX, y + originY, width, height); 4302 } 4303 void drawLine(int x1, int y1, int x2, int y2) { 4304 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 4305 } 4306 void drawText(int x, int y, string text) { 4307 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 4308 } 4309 void setOutlineColor(int r, int g, int b) { 4310 p.pen = Pen(Color(r,g,b), 1); 4311 } 4312 void setFillColor(int r, int g, int b) { 4313 p.fillColor = Color(r,g,b); 4314 } 4315 } 4316 4317 ScriptableVersion toArsdJsvar() { 4318 auto sv = new ScriptableVersion; 4319 sv.p = this.screenPainter.impl; 4320 sv.originX = this.screenPainter.originX; 4321 sv.originY = this.screenPainter.originY; 4322 return sv; 4323 } 4324 4325 static WidgetPainter fromJsVar(T)(T t) { 4326 return WidgetPainter.init; 4327 } 4328 // done.......... 4329 } 4330 4331 4332 struct Style { 4333 static struct helper(string m, T) { 4334 enum method = m; 4335 T v; 4336 4337 mixin template MethodOverride(typeof(this) v) { 4338 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 4339 } 4340 } 4341 4342 static auto opDispatch(string method, T)(T value) { 4343 return helper!(method, T)(value); 4344 } 4345 } 4346 4347 /++ 4348 Implementation detail of the [ControlledBy] UDA. 4349 4350 History: 4351 Added Oct 28, 2020 4352 +/ 4353 struct ControlledBy_(T, Args...) { 4354 Args args; 4355 4356 static if(Args.length) 4357 this(Args args) { 4358 this.args = args; 4359 } 4360 4361 private T construct(Widget parent) { 4362 return new T(args, parent); 4363 } 4364 } 4365 4366 /++ 4367 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 4368 4369 History: 4370 Added Oct 28, 2020 4371 +/ 4372 auto ControlledBy(T, Args...)(Args args) { 4373 return ControlledBy_!(T, Args)(args); 4374 } 4375 4376 struct ContainerMeta { 4377 string name; 4378 ContainerMeta[] children; 4379 Widget function(Widget parent) factory; 4380 4381 Widget instantiate(Widget parent) { 4382 auto n = factory(parent); 4383 n.name = name; 4384 foreach(child; children) 4385 child.instantiate(n); 4386 return n; 4387 } 4388 } 4389 4390 /++ 4391 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 4392 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 4393 4394 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 4395 structures. It works fine on structs declared inside functions though. 4396 4397 See: https://issues.dlang.org/show_bug.cgi?id=21984 4398 +/ 4399 template Container(CArgs...) { 4400 static if(CArgs.length && is(CArgs[0] : Widget)) { 4401 private alias Super = CArgs[0]; 4402 private alias CArgs2 = CArgs[1 .. $]; 4403 } else { 4404 private alias Super = Layout; 4405 private alias CArgs2 = CArgs; 4406 } 4407 4408 class Container : Super { 4409 this(Widget parent) { super(parent); } 4410 4411 // just to partially support old gdc versions 4412 version(GNU) { 4413 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 4414 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 4415 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 4416 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 4417 } else mixin(q{ 4418 static foreach(Arg; CArgs2) { 4419 mixin Arg.MethodOverride!(Arg); 4420 } 4421 }); 4422 4423 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 4424 return ContainerMeta( 4425 name, 4426 children.dup, 4427 function (Widget parent) { return new typeof(this)(parent); } 4428 ); 4429 } 4430 4431 static ContainerMeta opCall(ContainerMeta[] children...) { 4432 return opCall(null, children); 4433 } 4434 } 4435 } 4436 4437 /++ 4438 The data controller widget is created by reflecting over the given 4439 data type. You can use [ControlledBy] as a UDA on a struct or 4440 just let it create things automatically. 4441 4442 Unlike [dialog], this uses real-time updating of the data and 4443 you add it to another window yourself. 4444 4445 --- 4446 struct Test { 4447 int x; 4448 int y; 4449 } 4450 4451 auto window = new Window(); 4452 auto dcw = new DataControllerWidget!Test(new Test, window); 4453 --- 4454 4455 The way it works is any public members are given a widget based 4456 on their data type, and public methods trigger an action button 4457 if no relevant parameters or a dialog action if it does have 4458 parameters, similar to the [menu] facility. 4459 4460 If you change data programmatically, without going through the 4461 DataControllerWidget methods, you will have to tell it something 4462 has changed and it needs to redraw. This is done with the `invalidate` 4463 method. 4464 4465 History: 4466 Added Oct 28, 2020 4467 +/ 4468 /// Group: generating_from_code 4469 class DataControllerWidget(T) : WidgetContainer { 4470 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 4471 private alias Tref = T; 4472 else 4473 private alias Tref = T*; 4474 4475 Tref datum; 4476 4477 /++ 4478 See_also: [addDataControllerWidget] 4479 +/ 4480 this(Tref datum, Widget parent) { 4481 this.datum = datum; 4482 4483 Widget cp = this; 4484 4485 super(parent); 4486 4487 foreach(attr; __traits(getAttributes, T)) 4488 static if(is(typeof(attr) == ContainerMeta)) { 4489 cp = attr.instantiate(this); 4490 } 4491 4492 auto def = this.getByName("default"); 4493 if(def !is null) 4494 cp = def; 4495 4496 Widget helper(string name) { 4497 auto maybe = this.getByName(name); 4498 if(maybe is null) 4499 return cp; 4500 return maybe; 4501 4502 } 4503 4504 foreach(member; __traits(allMembers, T)) 4505 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4506 static if(is(typeof(__traits(getMember, this.datum, member)))) 4507 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4508 void delegate() update; 4509 4510 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4511 4512 if(update) 4513 updaters ~= update; 4514 4515 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4516 w.addEventListener("triggered", delegate() { 4517 makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))(); 4518 notifyDataUpdated(); 4519 }); 4520 } else static if(is(typeof(w.isChecked) == bool)) { 4521 w.addEventListener(EventType.change, (Event ev) { 4522 __traits(getMember, this.datum, member) = w.isChecked; 4523 }); 4524 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4525 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4526 } else static if(is(typeof(w.value) == int)) { 4527 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4528 } else static if(is(typeof(w) == DropDownSelection)) { 4529 // special case for this to kinda support enums and such. coudl be better though 4530 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4531 } else { 4532 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4533 } 4534 } 4535 } 4536 4537 /++ 4538 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4539 4540 History: 4541 Added May 28, 2021 4542 +/ 4543 void notifyDataUpdated() { 4544 foreach(updater; updaters) 4545 updater(); 4546 4547 this.emit!(ChangeEvent!void)(delegate{}); 4548 } 4549 4550 private Widget[string] memberWidgets; 4551 private void delegate()[] updaters; 4552 4553 mixin Emits!(ChangeEvent!void); 4554 } 4555 4556 private int saturatedSum(int[] values...) { 4557 int sum; 4558 foreach(value; values) { 4559 if(value == int.max) 4560 return int.max; 4561 sum += value; 4562 } 4563 return sum; 4564 } 4565 4566 void genericSetValue(T, W)(T* where, W what) { 4567 import std.conv; 4568 *where = to!T(what); 4569 //*where = cast(T) stringToLong(what); 4570 } 4571 4572 /++ 4573 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4574 4575 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4576 4577 Note that this creates the widget but does not attach any event handlers to it. 4578 +/ 4579 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4580 4581 string displayName = __traits(identifier, tt).beautify; 4582 4583 static if(controlledByCount!tt == 1) { 4584 foreach(i, attr; __traits(getAttributes, tt)) { 4585 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4586 auto w = attr.construct(parent); 4587 static if(__traits(compiles, w.setPosition(*valptr))) 4588 update = () { w.setPosition(*valptr); }; 4589 else static if(__traits(compiles, w.setValue(*valptr))) 4590 update = () { w.setValue(*valptr); }; 4591 4592 if(update) 4593 update(); 4594 return w; 4595 } 4596 } 4597 } else static if(controlledByCount!tt == 0) { 4598 static if(is(typeof(tt) == enum)) { 4599 // FIXME: update 4600 auto dds = new DropDownSelection(parent); 4601 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4602 dds.addOption(option); 4603 if(__traits(getMember, typeof(tt), option) == *valptr) 4604 dds.setSelection(cast(int) idx); 4605 } 4606 return dds; 4607 } else static if(is(typeof(tt) == bool)) { 4608 auto box = new Checkbox(displayName, parent); 4609 update = () { box.isChecked = *valptr; }; 4610 update(); 4611 return box; 4612 } else static if(is(typeof(tt) : const long)) { 4613 auto le = new LabeledLineEdit(displayName, parent); 4614 update = () { le.content = toInternal!string(*valptr); }; 4615 update(); 4616 return le; 4617 } else static if(is(typeof(tt) : const double)) { 4618 auto le = new LabeledLineEdit(displayName, parent); 4619 import std.conv; 4620 update = () { le.content = to!string(*valptr); }; 4621 update(); 4622 return le; 4623 } else static if(is(typeof(tt) : const string)) { 4624 auto le = new LabeledLineEdit(displayName, parent); 4625 update = () { le.content = *valptr; }; 4626 update(); 4627 return le; 4628 } else static if(is(typeof(tt) == function)) { 4629 auto w = new Button(displayName, parent); 4630 return w; 4631 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4632 return parent.addDataControllerWidget(tt); 4633 } else static assert(0, typeof(tt).stringof); 4634 } else static assert(0, "multiple controllers not yet supported"); 4635 } 4636 4637 private template controlledByCount(alias tt) { 4638 static int helper() { 4639 int count; 4640 foreach(i, attr; __traits(getAttributes, tt)) 4641 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4642 count++; 4643 return count; 4644 } 4645 4646 enum controlledByCount = helper; 4647 } 4648 4649 /++ 4650 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4651 4652 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4653 4654 History: 4655 The `redrawOnChange` parameter was added on May 28, 2021. 4656 +/ 4657 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4658 auto dcw = new DataControllerWidget!T(t, parent); 4659 initializeDataControllerWidget(dcw, redrawOnChange); 4660 return dcw; 4661 } 4662 4663 /// ditto 4664 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4665 auto dcw = new DataControllerWidget!T(t, parent); 4666 initializeDataControllerWidget(dcw, redrawOnChange); 4667 return dcw; 4668 } 4669 4670 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4671 if(redrawOnChange !is null) 4672 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4673 } 4674 4675 /++ 4676 Get this through [Widget.getComputedStyle]. It provides access to the [Widget.Style] style hints and [Widget] layout hints, possibly modified through the [VisualTheme], through a unifed interface. 4677 4678 History: 4679 Finalized on June 3, 2021 for the dub v10.0 release 4680 +/ 4681 struct StyleInformation { 4682 private Widget w; 4683 private BaseVisualTheme visualTheme; 4684 4685 private this(Widget w) { 4686 this.w = w; 4687 this.visualTheme = WidgetPainter.visualTheme; 4688 } 4689 4690 /++ 4691 Forwards to [Widget.Style] 4692 4693 Bugs: 4694 It is supposed to fall back to the [VisualTheme] if 4695 the style doesn't override the default, but that is 4696 not generally implemented. Many of them may end up 4697 being explicit overloads instead of the generic 4698 opDispatch fallback, like [font] is now. 4699 +/ 4700 public @property opDispatch(string name)() { 4701 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4702 w.useStyleProperties((scope Widget.Style props) { 4703 //visualTheme.useStyleProperties(w, (props) { 4704 prop = __traits(getMember, props, name); 4705 }); 4706 return prop; 4707 } 4708 4709 /++ 4710 Returns the cached font object associated with the widget, 4711 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4712 4713 History: 4714 Prior to March 21, 2022 (dub v10.7), `font` went through 4715 [opDispatch], which did not use the cache. You can now call it 4716 repeatedly without guilt. 4717 +/ 4718 public @property OperatingSystemFont font() { 4719 OperatingSystemFont prop; 4720 w.useStyleProperties((scope Widget.Style props) { 4721 prop = props.fontCached; 4722 }); 4723 if(prop is null) { 4724 prop = visualTheme.defaultFontCached(w.currentDpi); 4725 } 4726 return prop; 4727 } 4728 4729 @property { 4730 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4731 /** */ int paddingLeft() { return w.paddingLeft(); } 4732 /** */ int paddingRight() { return w.paddingRight(); } 4733 /** */ int paddingTop() { return w.paddingTop(); } 4734 /** */ int paddingBottom() { return w.paddingBottom(); } 4735 4736 /** */ int marginLeft() { return w.marginLeft(); } 4737 /** */ int marginRight() { return w.marginRight(); } 4738 /** */ int marginTop() { return w.marginTop(); } 4739 /** */ int marginBottom() { return w.marginBottom(); } 4740 4741 /** */ int maxHeight() { return w.maxHeight(); } 4742 /** */ int minHeight() { return w.minHeight(); } 4743 4744 /** */ int maxWidth() { return w.maxWidth(); } 4745 /** */ int minWidth() { return w.minWidth(); } 4746 4747 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4748 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4749 4750 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4751 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4752 4753 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4754 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4755 4756 // Global helpers some of these are unstable. 4757 static: 4758 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4759 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4760 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4761 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4762 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4763 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4764 4765 /** */ Color activeTabColor() { return lightAccentColor; } 4766 /** */ Color buttonColor() { return windowBackgroundColor; } 4767 /** */ Color depressedButtonColor() { return darkAccentColor; } 4768 /** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; } 4769 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4770 auto c = WidgetPainter.visualTheme.selectionColor(); 4771 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4772 } 4773 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4774 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4775 } 4776 4777 4778 4779 /+ 4780 4781 private static auto extractStyleProperty(string name)(Widget w) { 4782 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4783 w.useStyleProperties((props) { 4784 prop = __traits(getMember, props, name); 4785 }); 4786 return prop; 4787 } 4788 4789 // FIXME: clear this upon a X server disconnect 4790 private static OperatingSystemFont[string] fontCache; 4791 4792 T getProperty(T)(string name, lazy T default_) { 4793 if(visualTheme !is null) { 4794 auto str = visualTheme.getPropertyString(w, name); 4795 if(str is null) 4796 return default_; 4797 static if(is(T == Color)) 4798 return Color.fromString(str); 4799 else static if(is(T == Measurement)) 4800 return Measurement(cast(int) toInternal!int(str)); 4801 else static if(is(T == WidgetBackground)) 4802 return WidgetBackground.fromString(str); 4803 else static if(is(T == OperatingSystemFont)) { 4804 if(auto f = str in fontCache) 4805 return *f; 4806 else 4807 return fontCache[str] = new OperatingSystemFont(str); 4808 } else static if(is(T == FrameStyle)) { 4809 switch(str) { 4810 default: 4811 return FrameStyle.none; 4812 foreach(style; __traits(allMembers, FrameStyle)) 4813 case style: 4814 return __traits(getMember, FrameStyle, style); 4815 } 4816 } else static assert(0); 4817 } else 4818 return default_; 4819 } 4820 4821 static struct Measurement { 4822 int value; 4823 alias value this; 4824 } 4825 4826 @property: 4827 4828 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4829 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4830 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4831 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4832 4833 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4834 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4835 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4836 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4837 4838 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4839 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4840 4841 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4842 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4843 4844 4845 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4846 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4847 4848 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4849 4850 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4851 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4852 4853 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4854 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4855 4856 4857 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4858 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4859 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4860 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4861 4862 Color activeTabColor() { return lightAccentColor; } 4863 Color buttonColor() { return windowBackgroundColor; } 4864 Color depressedButtonColor() { return darkAccentColor; } 4865 Color hoveringColor() { return Color(228, 228, 228); } 4866 Color activeListXorColor() { 4867 auto c = WidgetPainter.visualTheme.selectionColor(); 4868 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4869 } 4870 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4871 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4872 +/ 4873 } 4874 4875 4876 4877 // pragma(msg, __traits(classInstanceSize, Widget)); 4878 4879 /*private*/ template EventString(E) { 4880 static if(is(typeof(E.EventString))) 4881 enum EventString = E.EventString; 4882 else 4883 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4884 } 4885 4886 /*private*/ template EventStringIdentifier(E) { 4887 string helper() { 4888 auto es = EventString!E; 4889 char[] id = new char[](es.length * 2); 4890 size_t idx; 4891 foreach(char ch; es) { 4892 id[idx++] = cast(char)('a' + (ch >> 4)); 4893 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4894 } 4895 return cast(string) id; 4896 } 4897 4898 enum EventStringIdentifier = helper(); 4899 } 4900 4901 4902 template classStaticallyEmits(This, EventType) { 4903 static if(is(This Base == super)) 4904 static if(is(Base : Widget)) 4905 enum baseEmits = classStaticallyEmits!(Base, EventType); 4906 else 4907 enum baseEmits = false; 4908 else 4909 enum baseEmits = false; 4910 4911 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4912 4913 enum classStaticallyEmits = thisEmits || baseEmits; 4914 } 4915 4916 /++ 4917 A helper to make widgets out of other native windows. 4918 4919 History: 4920 Factored out of OpenGlWidget on November 5, 2021 4921 +/ 4922 class NestedChildWindowWidget : Widget { 4923 SimpleWindow win; 4924 4925 /++ 4926 Used on X to send focus to the appropriate child window when requested by the window manager. 4927 4928 Normally returns its own nested window. Can also return another child or null to revert to the parent 4929 if you override it in a child class. 4930 4931 History: 4932 Added April 2, 2022 (dub v10.8) 4933 +/ 4934 SimpleWindow focusableWindow() { 4935 return win; 4936 } 4937 4938 /// 4939 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4940 this(SimpleWindow win, Widget parent) { 4941 this.parentWindow = parent.parentWindow; 4942 this.win = win; 4943 4944 super(parent); 4945 windowsetup(win); 4946 } 4947 4948 static protected SimpleWindow getParentWindow(Widget parent) { 4949 assert(parent !is null); 4950 SimpleWindow pwin = parent.parentWindow.win; 4951 4952 version(win32_widgets) { 4953 HWND phwnd; 4954 auto wtf = parent; 4955 while(wtf) { 4956 if(wtf.hwnd) { 4957 phwnd = wtf.hwnd; 4958 break; 4959 } 4960 wtf = wtf.parent; 4961 } 4962 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4963 if(phwnd) 4964 pwin = new SimpleWindow(phwnd); 4965 } 4966 4967 return pwin; 4968 } 4969 4970 /++ 4971 Called upon the nested window being destroyed. 4972 Remember the window has already been destroyed at 4973 this point, so don't use the native handle for anything. 4974 4975 History: 4976 Added April 3, 2022 (dub v10.8) 4977 +/ 4978 protected void dispose() { 4979 4980 } 4981 4982 protected void windowsetup(SimpleWindow w) { 4983 /* 4984 win.onFocusChange = (bool getting) { 4985 if(getting) 4986 this.focus(); 4987 }; 4988 */ 4989 4990 /+ 4991 win.onFocusChange = (bool getting) { 4992 if(getting) { 4993 this.parentWindow.focusedWidget = this; 4994 this.emit!FocusEvent(); 4995 this.emit!FocusInEvent(); 4996 } else { 4997 this.emit!BlurEvent(); 4998 this.emit!FocusOutEvent(); 4999 } 5000 }; 5001 +/ 5002 5003 win.onDestroyed = () { 5004 this.dispose(); 5005 }; 5006 5007 version(win32_widgets) { 5008 Widget.nativeMapping[win.hwnd] = this; 5009 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 5010 } else { 5011 win.setEventHandlers( 5012 (MouseEvent e) { 5013 Widget p = this; 5014 while(p ! is parentWindow) { 5015 e.x += p.x; 5016 e.y += p.y; 5017 p = p.parent; 5018 } 5019 parentWindow.dispatchMouseEvent(e); 5020 }, 5021 (KeyEvent e) { 5022 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 5023 parentWindow.dispatchKeyEvent(e); 5024 }, 5025 (dchar e) { 5026 parentWindow.dispatchCharEvent(e); 5027 }, 5028 ); 5029 } 5030 5031 } 5032 5033 override bool showOrHideIfNativeWindow(bool shouldShow) { 5034 auto cur = hidden; 5035 win.hidden = !shouldShow; 5036 if(cur != shouldShow && shouldShow) 5037 redraw(); 5038 return true; 5039 } 5040 5041 /// OpenGL widgets cannot have child widgets. Do not call this. 5042 /* @disable */ final override void addChild(Widget, int) { 5043 throw new Error("cannot add children to OpenGL widgets"); 5044 } 5045 5046 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 5047 /// Keep in mind that events like mouse coordinates are still relative to your size. 5048 override void registerMovement() { 5049 // writefln("%d %d %d %d", x,y,width,height); 5050 version(win32_widgets) 5051 auto pos = getChildPositionRelativeToParentHwnd(this); 5052 else 5053 auto pos = getChildPositionRelativeToParentOrigin(this); 5054 win.moveResize(pos[0], pos[1], width, height); 5055 5056 registerMovementAdditionalWork(); 5057 sendResizeEvent(); 5058 } 5059 5060 abstract void registerMovementAdditionalWork(); 5061 } 5062 5063 /++ 5064 Nests an opengl capable window inside this window as a widget. 5065 5066 You may also just want to create an additional [SimpleWindow] with 5067 [OpenGlOptions.yes] yourself. 5068 5069 An OpenGL widget cannot have child widgets. It will throw if you try. 5070 +/ 5071 static if(OpenGlEnabled) 5072 class OpenGlWidget : NestedChildWindowWidget { 5073 5074 override void registerMovementAdditionalWork() { 5075 win.setAsCurrentOpenGlContext(); 5076 } 5077 5078 /// 5079 this(Widget parent) { 5080 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5081 super(win, parent); 5082 } 5083 5084 override void paint(WidgetPainter painter) { 5085 win.setAsCurrentOpenGlContext(); 5086 glViewport(0, 0, this.width, this.height); 5087 win.redrawOpenGlSceneNow(); 5088 } 5089 5090 void redrawOpenGlScene(void delegate() dg) { 5091 win.redrawOpenGlScene = dg; 5092 } 5093 } 5094 5095 /++ 5096 This demo shows how to draw text in an opengl scene. 5097 +/ 5098 unittest { 5099 import arsd.minigui; 5100 import arsd.ttf; 5101 5102 void main() { 5103 auto window = new Window(); 5104 5105 auto widget = new OpenGlWidget(window); 5106 5107 // old means non-shader code so compatible with glBegin etc. 5108 // tbh I haven't implemented new one in font yet... 5109 // anyway, declaring here, will construct soon. 5110 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 5111 5112 // this is a little bit awkward, calling some methods through 5113 // the underlying SimpleWindow `win` method, and you can't do this 5114 // on a nanovega widget due to conflicts so I should probably fix 5115 // the api to be a bit easier. But here it will work. 5116 // 5117 // Alternatively, you could load the font on the first draw, inside 5118 // the redrawOpenGlScene, and keep a flag so you don't do it every 5119 // time. That'd be a bit easier since the lib sets up the context 5120 // by then guaranteed. 5121 // 5122 // But still, I wanna show this. 5123 widget.win.visibleForTheFirstTime = delegate { 5124 // must set the opengl context 5125 widget.win.setAsCurrentOpenGlContext(); 5126 5127 // if you were doing a OpenGL 3+ shader, this 5128 // gets especially important to do in order. With 5129 // old-style opengl, I think you can even do it 5130 // in main(), but meh, let's show it more correctly. 5131 5132 // Anyway, now it is time to load the font from the 5133 // OS (you can alternatively load one from a .ttf file 5134 // you bundle with the application), then load the 5135 // font into texture for drawing. 5136 5137 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 5138 5139 assert(!osfont.isNull()); // make sure it actually loaded 5140 5141 // using typeof to avoid repeating the long name lol 5142 glfont = new typeof(glfont)( 5143 // get the raw data from the font for loading in here 5144 // since it doesn't use the OS function to draw the 5145 // text, we gotta treat it more as a file than as 5146 // a drawing api. 5147 osfont.getTtfBytes(), 5148 18, // need to respecify size since opengl world is different coordinate system 5149 5150 // these last two numbers are why it is called 5151 // "Limited" font. It only loads the characters 5152 // in the given range, since the texture atlas 5153 // it references is all a big image generated ahead 5154 // of time. You could maybe do the whole thing but 5155 // idk how much memory that is. 5156 // 5157 // But here, 0-128 represents the ASCII range, so 5158 // good enough for most English things, numeric labels, 5159 // etc. 5160 0, 5161 128 5162 ); 5163 }; 5164 5165 widget.redrawOpenGlScene = () { 5166 // now we can use the glfont's drawString function 5167 5168 // first some opengl setup. You can do this in one place 5169 // on window first visible too in many cases, just showing 5170 // here cuz it is easier for me. 5171 5172 // gonna need some alpha blending or it just looks awful 5173 glEnable(GL_BLEND); 5174 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5175 glClearColor(0,0,0,0); 5176 glDepthFunc(GL_LEQUAL); 5177 5178 // Also need to enable 2d textures, since it draws the 5179 // font characters as images baked in 5180 glMatrixMode(GL_MODELVIEW); 5181 glLoadIdentity(); 5182 glDisable(GL_DEPTH_TEST); 5183 glEnable(GL_TEXTURE_2D); 5184 5185 // the orthographic matrix is best for 2d things like text 5186 // so let's set that up. This matrix makes the coordinates 5187 // in the opengl scene be one-to-one with the actual pixels 5188 // on screen. (Not necessarily best, you may wish to scale 5189 // things, but it does help keep fonts looking normal.) 5190 glMatrixMode(GL_PROJECTION); 5191 glLoadIdentity(); 5192 glOrtho(0, widget.width, widget.height, 0, 0, 1); 5193 5194 // you can do other glScale, glRotate, glTranslate, etc 5195 // to the matrix here of course if you want. 5196 5197 // note the x,y coordinates here are for the text baseline 5198 // NOT the upper-left corner. The baseline is like the line 5199 // in the notebook you write on. Most the letters are actually 5200 // above it, but some, like p and q, dip a bit below it. 5201 // 5202 // So if you're used to the upper left coordinate like the 5203 // rest of simpledisplay/minigui usually do, do the 5204 // y + glfont.ascent to bring it down a little. So this 5205 // example puts the string in the upper left of the window. 5206 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 5207 5208 // re color btw: the function sets a solid color internally, 5209 // but you actually COULD do your own thing for rainbow effects 5210 // and the sort if you wanted too, by pulling its guts out. 5211 // Just view its source for an idea of how it actually draws: 5212 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 5213 5214 // it gets a bit complicated with the character positioning, 5215 // but the opengl parts are fairly simple: bind a texture, 5216 // set the color, draw a quad for each letter. 5217 5218 5219 // the last optional argument there btw is a bounding box 5220 // it will/ use to word wrap and return an object you can 5221 // use to implement scrolling or pagination; it tells how 5222 // much of the string didn't fit in the box. But for simple 5223 // labels we can just ignore that. 5224 5225 5226 // I'd suggest drawing text as the last step, after you 5227 // do your other drawing. You might use the push/pop matrix 5228 // stuff to keep your place. You, in theory, should be able 5229 // to do text in a 3d space but I've never actually tried 5230 // that.... 5231 }; 5232 5233 window.loop(); 5234 } 5235 } 5236 5237 version(custom_widgets) 5238 private class TextListViewWidget : GenericListViewWidget { 5239 static class TextListViewItem : GenericListViewItem { 5240 ListWidget controller; 5241 this(ListWidget controller, Widget parent) { 5242 this.controller = controller; 5243 this.tabStop = false; 5244 super(parent); 5245 } 5246 5247 ListWidget.Option* showing; 5248 5249 override void showItem(int idx) { 5250 showing = idx < controller.options.length ? &controller.options[idx] : null; 5251 redraw(); // is this necessary? the generic thing might call it... 5252 } 5253 5254 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 5255 if(showing is null) 5256 return bounds; 5257 painter.drawText(bounds.upperLeft, showing.label); 5258 return bounds; 5259 } 5260 5261 static class Style : Widget.Style { 5262 override WidgetBackground background() { 5263 // FIXME: change it if it is focused or not 5264 // needs to reliably detect if focused (noting the actual focus may be on a parent or child... or even sibling for FreeEntrySelection. maybe i just need a better way to proxy focus in widgets generically). also will need to redraw correctly without defaultEventHandler_focusin hacks like EditableTextWidget uses 5265 auto tlvi = cast(TextListViewItem) widget; 5266 if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected) 5267 return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode 5268 return super.background(); 5269 } 5270 5271 override Color foregroundColor() { 5272 auto tlvi = cast(TextListViewItem) widget; 5273 return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor(); 5274 } 5275 5276 override FrameStyle outlineStyle() { 5277 // FIXME: change it if it is focused or not 5278 auto tlvi = cast(TextListViewItem) widget; 5279 return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle(); 5280 } 5281 } 5282 mixin OverrideStyle!Style; 5283 5284 mixin Padding!q{2}; 5285 5286 override void defaultEventHandler_click(ClickEvent event) { 5287 if(event.button == MouseButton.left) { 5288 controller.setSelection(currentIndexLoaded()); 5289 controller.focusOn = currentIndexLoaded(); 5290 } 5291 } 5292 5293 } 5294 5295 ListWidget controller; 5296 5297 this(ListWidget parent) { 5298 this.controller = parent; 5299 this.tabStop = false; // this is only used as a child of the ListWidget 5300 super(parent); 5301 5302 smw.movementPerButtonClick(1, itemSize().height); 5303 } 5304 5305 override Size itemSize() { 5306 return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */)); 5307 } 5308 5309 override GenericListViewItem itemFactory(Widget parent) { 5310 return new TextListViewItem(controller, parent); 5311 } 5312 5313 static class Style : Widget.Style { 5314 override FrameStyle borderStyle() { 5315 return FrameStyle.sunk; 5316 } 5317 5318 override WidgetBackground background() { 5319 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 5320 } 5321 } 5322 mixin OverrideStyle!Style; 5323 } 5324 5325 /++ 5326 A list widget contains a list of strings that the user can examine and select. 5327 5328 5329 In the future, items in the list may be possible to be more than just strings. 5330 5331 See_Also: 5332 [TableView] 5333 +/ 5334 class ListWidget : Widget { 5335 /// 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. 5336 mixin Emits!(ChangeEvent!void); 5337 5338 version(custom_widgets) 5339 TextListViewWidget glvw; 5340 5341 static struct Option { 5342 string label; 5343 bool selected; 5344 void* tag; 5345 } 5346 private Option[] options; 5347 5348 /++ 5349 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 5350 +/ 5351 void setSelection(int y) { 5352 if(!multiSelect) 5353 foreach(ref opt; options) 5354 opt.selected = false; 5355 if(y >= 0 && y < options.length) 5356 options[y].selected = !options[y].selected; 5357 5358 version(custom_widgets) 5359 focusOn = y; 5360 5361 this.emit!(ChangeEvent!void)(delegate {}); 5362 5363 version(custom_widgets) 5364 redraw(); 5365 } 5366 5367 /++ 5368 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 5369 Returns -1 if nothing is selected. 5370 +/ 5371 int getSelection() 5372 { 5373 foreach(i, opt; options) { 5374 if (opt.selected) 5375 return cast(int) i; 5376 } 5377 return -1; 5378 } 5379 5380 version(custom_widgets) 5381 private int focusOn; 5382 5383 this(Widget parent) { 5384 super(parent); 5385 5386 version(custom_widgets) 5387 glvw = new TextListViewWidget(this); 5388 5389 version(win32_widgets) 5390 createWin32Window(this, WC_LISTBOX, "", 5391 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 5392 } 5393 5394 version(win32_widgets) 5395 override void handleWmCommand(ushort code, ushort id) { 5396 switch(code) { 5397 case LBN_SELCHANGE: 5398 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 5399 setSelection(cast(int) sel); 5400 break; 5401 default: 5402 } 5403 } 5404 5405 5406 void addOption(string text, void* tag = null) { 5407 options ~= Option(text, false, tag); 5408 version(win32_widgets) { 5409 WCharzBuffer buffer = WCharzBuffer(text); 5410 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 5411 } 5412 version(custom_widgets) { 5413 glvw.setItemCount(cast(int) options.length); 5414 //setContentSize(width, cast(int) (options.length * defaultLineHeight)); 5415 redraw(); 5416 } 5417 } 5418 5419 void clear() { 5420 options = null; 5421 version(win32_widgets) { 5422 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 5423 {} 5424 5425 } else version(custom_widgets) { 5426 focusOn = -1; 5427 glvw.setItemCount(0); 5428 redraw(); 5429 } 5430 } 5431 5432 version(custom_widgets) 5433 override void defaultEventHandler_keydown(KeyDownEvent kde) { 5434 void changedFocusOn() { 5435 scrollFocusIntoView(); 5436 if(multiSelect) 5437 redraw(); 5438 else 5439 setSelection(focusOn); 5440 } 5441 switch(kde.key) { 5442 case Key.Up: 5443 if(focusOn) { 5444 focusOn--; 5445 changedFocusOn(); 5446 } 5447 break; 5448 case Key.Down: 5449 if(focusOn + 1 < options.length) { 5450 focusOn++; 5451 changedFocusOn(); 5452 } 5453 break; 5454 case Key.Home: 5455 if(focusOn) { 5456 focusOn = 0; 5457 changedFocusOn(); 5458 } 5459 break; 5460 case Key.End: 5461 if(options.length && focusOn + 1 != options.length) { 5462 focusOn = cast(int) options.length - 1; 5463 changedFocusOn(); 5464 } 5465 break; 5466 case Key.PageUp: 5467 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5468 focusOn -= n; 5469 if(focusOn < 0) 5470 focusOn = 0; 5471 changedFocusOn(); 5472 break; 5473 case Key.PageDown: 5474 if(options.length == 0) 5475 break; 5476 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5477 focusOn += n; 5478 if(focusOn >= options.length) 5479 focusOn = cast(int) options.length - 1; 5480 changedFocusOn(); 5481 break; 5482 5483 default: 5484 } 5485 } 5486 5487 version(custom_widgets) 5488 override void defaultEventHandler_char(CharEvent ce) { 5489 if(ce.character == '\n' || ce.character == ' ') { 5490 setSelection(focusOn); 5491 } else { 5492 // search for the item that best matches and jump to it 5493 // FIXME this sucks in tons of ways. the normal thing toolkits 5494 // do here is to search for a substring on a timer, but i'd kinda 5495 // rather make an actual little dialog with some options. still meh for now. 5496 dchar search = ce.character; 5497 if(search >= 'A' && search <= 'Z') 5498 search += 32; 5499 foreach(idx, option; options) { 5500 auto ch = option.label.length ? option.label[0] : 0; 5501 if(ch >= 'A' && ch <= 'Z') 5502 ch += 32; 5503 if(ch == search) { 5504 setSelection(cast(int) idx); 5505 scrollSelectionIntoView(); 5506 break; 5507 } 5508 } 5509 5510 } 5511 } 5512 5513 version(win32_widgets) 5514 enum multiSelect = false; /// not implemented yet 5515 else 5516 bool multiSelect; 5517 5518 override int heightStretchiness() { return 6; } 5519 5520 version(custom_widgets) 5521 void scrollFocusIntoView() { 5522 glvw.ensureItemVisibleInScroll(focusOn); 5523 } 5524 5525 void scrollSelectionIntoView() { 5526 // FIXME: implement on Windows 5527 5528 version(custom_widgets) 5529 glvw.ensureItemVisibleInScroll(getSelection()); 5530 } 5531 5532 /* 5533 version(custom_widgets) 5534 override void defaultEventHandler_focusout(Event foe) { 5535 glvw.redraw(); 5536 } 5537 5538 version(custom_widgets) 5539 override void defaultEventHandler_focusin(Event foe) { 5540 glvw.redraw(); 5541 } 5542 */ 5543 5544 } 5545 5546 5547 5548 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 5549 /// NEVER USED 5550 enum ScrollBarShowPolicy { 5551 automatic, /// automatically show the scroll bar if it is necessary 5552 never, /// never show the scroll bar (scrolling must be done programmatically) 5553 always /// always show the scroll bar, even if it is disabled 5554 } 5555 5556 /++ 5557 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 5558 5559 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 5560 +/ 5561 // FIXME ScrollBarShowPolicy 5562 // FIXME: use the ScrollMessageWidget in here now that it exists 5563 // deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it 5564 class ScrollableWidget : Widget { 5565 // FIXME: make line size configurable 5566 // FIXME: add keyboard controls 5567 version(win32_widgets) { 5568 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 5569 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 5570 auto pos = HIWORD(wParam); 5571 auto m = LOWORD(wParam); 5572 5573 // FIXME: I can reintroduce the 5574 // scroll bars now by using this 5575 // in the top-level window handler 5576 // to forward comamnds 5577 auto scrollbarHwnd = lParam; 5578 switch(m) { 5579 case SB_BOTTOM: 5580 if(msg == WM_HSCROLL) 5581 horizontalScrollTo(contentWidth_); 5582 else 5583 verticalScrollTo(contentHeight_); 5584 break; 5585 case SB_TOP: 5586 if(msg == WM_HSCROLL) 5587 horizontalScrollTo(0); 5588 else 5589 verticalScrollTo(0); 5590 break; 5591 case SB_ENDSCROLL: 5592 // idk 5593 break; 5594 case SB_LINEDOWN: 5595 if(msg == WM_HSCROLL) 5596 horizontalScroll(scaleWithDpi(16)); 5597 else 5598 verticalScroll(scaleWithDpi(16)); 5599 break; 5600 case SB_LINEUP: 5601 if(msg == WM_HSCROLL) 5602 horizontalScroll(scaleWithDpi(-16)); 5603 else 5604 verticalScroll(scaleWithDpi(-16)); 5605 break; 5606 case SB_PAGEDOWN: 5607 if(msg == WM_HSCROLL) 5608 horizontalScroll(scaleWithDpi(100)); 5609 else 5610 verticalScroll(scaleWithDpi(100)); 5611 break; 5612 case SB_PAGEUP: 5613 if(msg == WM_HSCROLL) 5614 horizontalScroll(scaleWithDpi(-100)); 5615 else 5616 verticalScroll(scaleWithDpi(-100)); 5617 break; 5618 case SB_THUMBPOSITION: 5619 case SB_THUMBTRACK: 5620 if(msg == WM_HSCROLL) 5621 horizontalScrollTo(pos); 5622 else 5623 verticalScrollTo(pos); 5624 5625 if(m == SB_THUMBTRACK) { 5626 // the event loop doesn't seem to carry on with a requested redraw.. 5627 // so we request it to get our dirty bit set... 5628 redraw(); 5629 5630 // then we need to immediately actually redraw it too for instant feedback to user 5631 5632 SimpleWindow.processAllCustomEvents(); 5633 //if(parentWindow) 5634 //parentWindow.actualRedraw(); 5635 } 5636 break; 5637 default: 5638 } 5639 } 5640 return super.hookedWndProc(msg, wParam, lParam); 5641 } 5642 } 5643 /// 5644 this(Widget parent) { 5645 this.parentWindow = parent.parentWindow; 5646 5647 version(win32_widgets) { 5648 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 5649 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 5650 super(parent); 5651 } else version(custom_widgets) { 5652 outerContainer = new InternalScrollableContainerWidget(this, parent); 5653 super(outerContainer); 5654 } else static assert(0); 5655 } 5656 5657 version(custom_widgets) 5658 InternalScrollableContainerWidget outerContainer; 5659 5660 override void defaultEventHandler_click(ClickEvent event) { 5661 if(event.button == MouseButton.wheelUp) 5662 verticalScroll(scaleWithDpi(-16)); 5663 if(event.button == MouseButton.wheelDown) 5664 verticalScroll(scaleWithDpi(16)); 5665 super.defaultEventHandler_click(event); 5666 } 5667 5668 override void defaultEventHandler_keydown(KeyDownEvent event) { 5669 switch(event.key) { 5670 case Key.Left: 5671 horizontalScroll(scaleWithDpi(-16)); 5672 break; 5673 case Key.Right: 5674 horizontalScroll(scaleWithDpi(16)); 5675 break; 5676 case Key.Up: 5677 verticalScroll(scaleWithDpi(-16)); 5678 break; 5679 case Key.Down: 5680 verticalScroll(scaleWithDpi(16)); 5681 break; 5682 case Key.Home: 5683 verticalScrollTo(0); 5684 break; 5685 case Key.End: 5686 verticalScrollTo(contentHeight); 5687 break; 5688 case Key.PageUp: 5689 verticalScroll(scaleWithDpi(-160)); 5690 break; 5691 case Key.PageDown: 5692 verticalScroll(scaleWithDpi(160)); 5693 break; 5694 default: 5695 } 5696 super.defaultEventHandler_keydown(event); 5697 } 5698 5699 5700 version(win32_widgets) 5701 override void recomputeChildLayout() { 5702 super.recomputeChildLayout(); 5703 SCROLLINFO info; 5704 info.cbSize = info.sizeof; 5705 info.nPage = viewportHeight; 5706 info.fMask = SIF_PAGE | SIF_RANGE; 5707 info.nMin = 0; 5708 info.nMax = contentHeight_; 5709 SetScrollInfo(hwnd, SB_VERT, &info, true); 5710 5711 info.cbSize = info.sizeof; 5712 info.nPage = viewportWidth; 5713 info.fMask = SIF_PAGE | SIF_RANGE; 5714 info.nMin = 0; 5715 info.nMax = contentWidth_; 5716 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5717 } 5718 5719 /* 5720 Scrolling 5721 ------------ 5722 5723 You are assigned a width and a height by the layout engine, which 5724 is your viewport box. However, you may draw more than that by setting 5725 a contentWidth and contentHeight. 5726 5727 If these can be contained by the viewport, no scrollbar is displayed. 5728 If they cannot fit though, it will automatically show scroll as necessary. 5729 5730 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5731 is zero, no vertical scrolling is performed. 5732 5733 If scrolling is necessary, the lib will automatically work with the bars. 5734 When you redraw, the origin and clipping info in the painter is set so if 5735 you just draw everything, it will work, but you can be more efficient by checking 5736 the viewportWidth, viewportHeight, and scrollOrigin members. 5737 */ 5738 5739 /// 5740 final @property int viewportWidth() { 5741 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5742 } 5743 /// 5744 final @property int viewportHeight() { 5745 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5746 } 5747 5748 // FIXME property 5749 Point scrollOrigin_; 5750 5751 /// 5752 final const(Point) scrollOrigin() { 5753 return scrollOrigin_; 5754 } 5755 5756 // the user sets these two 5757 private int contentWidth_ = 0; 5758 private int contentHeight_ = 0; 5759 5760 /// 5761 int contentWidth() { return contentWidth_; } 5762 /// 5763 int contentHeight() { return contentHeight_; } 5764 5765 /// 5766 void setContentSize(int width, int height) { 5767 contentWidth_ = width; 5768 contentHeight_ = height; 5769 5770 version(custom_widgets) { 5771 if(showingVerticalScroll || showingHorizontalScroll) { 5772 outerContainer.queueRecomputeChildLayout(); 5773 } 5774 5775 if(showingVerticalScroll()) 5776 outerContainer.verticalScrollBar.redraw(); 5777 if(showingHorizontalScroll()) 5778 outerContainer.horizontalScrollBar.redraw(); 5779 } else version(win32_widgets) { 5780 queueRecomputeChildLayout(); 5781 } else static assert(0); 5782 } 5783 5784 /// 5785 void verticalScroll(int delta) { 5786 verticalScrollTo(scrollOrigin.y + delta); 5787 } 5788 /// 5789 void verticalScrollTo(int pos) { 5790 scrollOrigin_.y = pos; 5791 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5792 scrollOrigin_.y = contentHeight - viewportHeight; 5793 5794 if(scrollOrigin_.y < 0) 5795 scrollOrigin_.y = 0; 5796 5797 version(win32_widgets) { 5798 SCROLLINFO info; 5799 info.cbSize = info.sizeof; 5800 info.fMask = SIF_POS; 5801 info.nPos = scrollOrigin_.y; 5802 SetScrollInfo(hwnd, SB_VERT, &info, true); 5803 } else version(custom_widgets) { 5804 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5805 } else static assert(0); 5806 5807 redraw(); 5808 } 5809 5810 /// 5811 void horizontalScroll(int delta) { 5812 horizontalScrollTo(scrollOrigin.x + delta); 5813 } 5814 /// 5815 void horizontalScrollTo(int pos) { 5816 scrollOrigin_.x = pos; 5817 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5818 scrollOrigin_.x = contentWidth - viewportWidth; 5819 5820 if(scrollOrigin_.x < 0) 5821 scrollOrigin_.x = 0; 5822 5823 version(win32_widgets) { 5824 SCROLLINFO info; 5825 info.cbSize = info.sizeof; 5826 info.fMask = SIF_POS; 5827 info.nPos = scrollOrigin_.x; 5828 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5829 } else version(custom_widgets) { 5830 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5831 } else static assert(0); 5832 5833 redraw(); 5834 } 5835 /// 5836 void scrollTo(Point p) { 5837 verticalScrollTo(p.y); 5838 horizontalScrollTo(p.x); 5839 } 5840 5841 /// 5842 void ensureVisibleInScroll(Point p) { 5843 auto rect = viewportRectangle(); 5844 if(rect.contains(p)) 5845 return; 5846 if(p.x < rect.left) 5847 horizontalScroll(p.x - rect.left); 5848 else if(p.x > rect.right) 5849 horizontalScroll(p.x - rect.right); 5850 5851 if(p.y < rect.top) 5852 verticalScroll(p.y - rect.top); 5853 else if(p.y > rect.bottom) 5854 verticalScroll(p.y - rect.bottom); 5855 } 5856 5857 /// 5858 void ensureVisibleInScroll(Rectangle rect) { 5859 ensureVisibleInScroll(rect.upperLeft); 5860 ensureVisibleInScroll(rect.lowerRight); 5861 } 5862 5863 /// 5864 Rectangle viewportRectangle() { 5865 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5866 } 5867 5868 /// 5869 bool showingHorizontalScroll() { 5870 return contentWidth > width; 5871 } 5872 /// 5873 bool showingVerticalScroll() { 5874 return contentHeight > height; 5875 } 5876 5877 /// This is called before the ordinary paint delegate, 5878 /// giving you a chance to draw the window frame, etc, 5879 /// before the scroll clip takes effect 5880 void paintFrameAndBackground(WidgetPainter painter) { 5881 version(win32_widgets) { 5882 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5883 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5884 // since the pen is null, to fill the whole space, we need the +1 on both. 5885 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5886 SelectObject(painter.impl.hdc, p); 5887 SelectObject(painter.impl.hdc, b); 5888 } 5889 5890 } 5891 5892 // make space for the scroll bar, and that's it. 5893 final override int paddingRight() { return scaleWithDpi(16); } 5894 final override int paddingBottom() { return scaleWithDpi(16); } 5895 5896 /* 5897 END SCROLLING 5898 */ 5899 5900 override WidgetPainter draw() { 5901 int x = this.x, y = this.y; 5902 auto parent = this.parent; 5903 while(parent) { 5904 x += parent.x; 5905 y += parent.y; 5906 parent = parent.parent; 5907 } 5908 5909 //version(win32_widgets) { 5910 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5911 //} else { 5912 auto painter = parentWindow.win.draw(true); 5913 //} 5914 painter.originX = x; 5915 painter.originY = y; 5916 5917 painter.originX = painter.originX - scrollOrigin.x; 5918 painter.originY = painter.originY - scrollOrigin.y; 5919 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5920 5921 return WidgetPainter(painter, this); 5922 } 5923 5924 mixin ScrollableChildren; 5925 } 5926 5927 // you need to have a Point scrollOrigin in the class somewhere 5928 // and a paintFrameAndBackground 5929 private mixin template ScrollableChildren() { 5930 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5931 if(hidden) 5932 return; 5933 5934 //version(win32_widgets) 5935 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5936 5937 painter.originX = lox + x; 5938 painter.originY = loy + y; 5939 5940 bool actuallyPainted = false; 5941 5942 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5943 if(clip == Rectangle.init) 5944 return; 5945 5946 if(force || redrawRequested) { 5947 //painter.setClipRectangle(scrollOrigin, width, height); 5948 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5949 paintFrameAndBackground(painter); 5950 } 5951 5952 /+ 5953 version(win32_widgets) { 5954 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 5955 } 5956 +/ 5957 5958 painter.originX = painter.originX - scrollOrigin.x; 5959 painter.originY = painter.originY - scrollOrigin.y; 5960 if(force || redrawRequested) { 5961 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5962 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5963 5964 //erase(painter); // we paintFrameAndBackground above so no need 5965 if(painter.visualTheme) 5966 painter.visualTheme.doPaint(this, painter); 5967 else 5968 paint(painter); 5969 5970 if(invalidate) { 5971 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5972 // children are contained inside this, so no need to do extra work 5973 invalidate = false; 5974 } 5975 5976 5977 actuallyPainted = true; 5978 redrawRequested = false; 5979 } 5980 5981 foreach(child; children) { 5982 if(cast(FixedPosition) child) 5983 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5984 else 5985 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5986 } 5987 } 5988 } 5989 5990 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5991 ScrollableContainerWidget scw; 5992 5993 this(ScrollableContainerWidget parent) { 5994 scw = parent; 5995 super(parent); 5996 } 5997 5998 version(custom_widgets) 5999 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6000 if(hidden) 6001 return; 6002 6003 bool actuallyPainted = false; 6004 6005 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 6006 6007 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 6008 if(clip == Rectangle.init) 6009 return; 6010 6011 painter.originX = lox + x - scrollOrigin.x; 6012 painter.originY = loy + y - scrollOrigin.y; 6013 if(force || redrawRequested) { 6014 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6015 6016 erase(painter); 6017 if(painter.visualTheme) 6018 painter.visualTheme.doPaint(this, painter); 6019 else 6020 paint(painter); 6021 6022 if(invalidate) { 6023 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6024 // children are contained inside this, so no need to do extra work 6025 invalidate = false; 6026 } 6027 6028 actuallyPainted = true; 6029 redrawRequested = false; 6030 } 6031 foreach(child; children) { 6032 if(cast(FixedPosition) child) 6033 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6034 else 6035 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6036 } 6037 } 6038 6039 version(custom_widgets) 6040 override protected void addScrollPosition(ref int x, ref int y) { 6041 x += scw.scrollX_; 6042 y += scw.scrollY_; 6043 } 6044 } 6045 6046 /++ 6047 A widget meant to contain other widgets that may need to scroll. 6048 6049 Currently buggy. 6050 6051 History: 6052 Added July 1, 2021 (dub v10.2) 6053 6054 On January 3, 2022, I tried to use it in a few other cases 6055 and found it only worked well in the original test case. Since 6056 it still sucks, I think I'm going to rewrite it again. 6057 +/ 6058 class ScrollableContainerWidget : ContainerWidget { 6059 /// 6060 this(Widget parent) { 6061 super(parent); 6062 6063 container = new InternalScrollableContainerInsideWidget(this); 6064 hsb = new HorizontalScrollbar(this); 6065 vsb = new VerticalScrollbar(this); 6066 6067 tabStop = false; 6068 container.tabStop = false; 6069 magic = true; 6070 6071 6072 vsb.addEventListener("scrolltonextline", () { 6073 scrollBy(0, scaleWithDpi(16)); 6074 }); 6075 vsb.addEventListener("scrolltopreviousline", () { 6076 scrollBy(0,scaleWithDpi( -16)); 6077 }); 6078 vsb.addEventListener("scrolltonextpage", () { 6079 scrollBy(0, container.height); 6080 }); 6081 vsb.addEventListener("scrolltopreviouspage", () { 6082 scrollBy(0, -container.height); 6083 }); 6084 vsb.addEventListener((scope ScrollToPositionEvent spe) { 6085 scrollTo(scrollX_, spe.value); 6086 }); 6087 6088 this.addEventListener(delegate (scope ClickEvent e) { 6089 if(e.button == MouseButton.wheelUp) { 6090 if(!e.defaultPrevented) 6091 scrollBy(0, scaleWithDpi(-16)); 6092 e.stopPropagation(); 6093 } else if(e.button == MouseButton.wheelDown) { 6094 if(!e.defaultPrevented) 6095 scrollBy(0, scaleWithDpi(16)); 6096 e.stopPropagation(); 6097 } 6098 }); 6099 } 6100 6101 /+ 6102 override void defaultEventHandler_click(ClickEvent e) { 6103 } 6104 +/ 6105 6106 override void removeAllChildren() { 6107 container.removeAllChildren(); 6108 } 6109 6110 void scrollTo(int x, int y) { 6111 scrollBy(x - scrollX_, y - scrollY_); 6112 } 6113 6114 void scrollBy(int x, int y) { 6115 auto ox = scrollX_; 6116 auto oy = scrollY_; 6117 6118 auto nx = ox + x; 6119 auto ny = oy + y; 6120 6121 if(nx < 0) 6122 nx = 0; 6123 if(ny < 0) 6124 ny = 0; 6125 6126 auto maxX = hsb.max - container.width; 6127 if(maxX < 0) maxX = 0; 6128 auto maxY = vsb.max - container.height; 6129 if(maxY < 0) maxY = 0; 6130 6131 if(nx > maxX) 6132 nx = maxX; 6133 if(ny > maxY) 6134 ny = maxY; 6135 6136 auto dx = nx - ox; 6137 auto dy = ny - oy; 6138 6139 if(dx || dy) { 6140 version(win32_widgets) 6141 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 6142 else { 6143 redraw(); 6144 } 6145 6146 hsb.setPosition = nx; 6147 vsb.setPosition = ny; 6148 6149 scrollX_ = nx; 6150 scrollY_ = ny; 6151 } 6152 } 6153 6154 private int scrollX_; 6155 private int scrollY_; 6156 6157 void setTotalArea(int width, int height) { 6158 hsb.setMax(width); 6159 vsb.setMax(height); 6160 } 6161 6162 /// 6163 void setViewableArea(int width, int height) { 6164 hsb.setViewableArea(width); 6165 vsb.setViewableArea(height); 6166 } 6167 6168 private bool magic; 6169 override void addChild(Widget w, int position = int.max) { 6170 if(magic) 6171 container.addChild(w, position); 6172 else 6173 super.addChild(w, position); 6174 } 6175 6176 override void recomputeChildLayout() { 6177 if(hsb is null || vsb is null || container is null) return; 6178 6179 /+ 6180 writeln(x, " ", y , " ", width, " ", height); 6181 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 6182 +/ 6183 6184 registerMovement(); 6185 6186 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 6187 hsb.x = 0; 6188 hsb.y = this.height - hsb.height; 6189 hsb.width = this.width - scaleWithDpi(16); 6190 hsb.recomputeChildLayout(); 6191 6192 vsb.width = scaleWithDpi(16); // FIXME? 6193 vsb.x = this.width - vsb.width; 6194 vsb.y = 0; 6195 vsb.height = this.height - scaleWithDpi(16); 6196 vsb.recomputeChildLayout(); 6197 6198 container.x = 0; 6199 container.y = 0; 6200 container.width = this.width - vsb.width; 6201 container.height = this.height - hsb.height; 6202 container.recomputeChildLayout(); 6203 6204 scrollX_ = 0; 6205 scrollY_ = 0; 6206 6207 hsb.setPosition(0); 6208 vsb.setPosition(0); 6209 6210 int mw, mh; 6211 Widget c = container; 6212 // FIXME: hack here to handle a layout inside... 6213 if(c.children.length == 1 && cast(Layout) c.children[0]) 6214 c = c.children[0]; 6215 foreach(child; c.children) { 6216 auto w = child.x + child.width; 6217 auto h = child.y + child.height; 6218 6219 if(w > mw) mw = w; 6220 if(h > mh) mh = h; 6221 } 6222 6223 setTotalArea(mw, mh); 6224 setViewableArea(width, height); 6225 } 6226 6227 override int minHeight() { return scaleWithDpi(64); } 6228 6229 HorizontalScrollbar hsb; 6230 VerticalScrollbar vsb; 6231 ContainerWidget container; 6232 } 6233 6234 6235 version(custom_widgets) 6236 // deprecated // i can't deprecate it w/o stupid messages ugh 6237 private class InternalScrollableContainerWidget : Widget { 6238 6239 ScrollableWidget sw; 6240 6241 VerticalScrollbar verticalScrollBar; 6242 HorizontalScrollbar horizontalScrollBar; 6243 6244 this(ScrollableWidget sw, Widget parent) { 6245 this.sw = sw; 6246 6247 this.tabStop = false; 6248 6249 super(parent); 6250 6251 horizontalScrollBar = new HorizontalScrollbar(this); 6252 verticalScrollBar = new VerticalScrollbar(this); 6253 6254 horizontalScrollBar.showing_ = false; 6255 verticalScrollBar.showing_ = false; 6256 6257 horizontalScrollBar.addEventListener("scrolltonextline", { 6258 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 6259 sw.horizontalScrollTo(horizontalScrollBar.position); 6260 }); 6261 horizontalScrollBar.addEventListener("scrolltopreviousline", { 6262 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 6263 sw.horizontalScrollTo(horizontalScrollBar.position); 6264 }); 6265 verticalScrollBar.addEventListener("scrolltonextline", { 6266 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 6267 sw.verticalScrollTo(verticalScrollBar.position); 6268 }); 6269 verticalScrollBar.addEventListener("scrolltopreviousline", { 6270 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 6271 sw.verticalScrollTo(verticalScrollBar.position); 6272 }); 6273 horizontalScrollBar.addEventListener("scrolltonextpage", { 6274 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 6275 sw.horizontalScrollTo(horizontalScrollBar.position); 6276 }); 6277 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 6278 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 6279 sw.horizontalScrollTo(horizontalScrollBar.position); 6280 }); 6281 verticalScrollBar.addEventListener("scrolltonextpage", { 6282 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 6283 sw.verticalScrollTo(verticalScrollBar.position); 6284 }); 6285 verticalScrollBar.addEventListener("scrolltopreviouspage", { 6286 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 6287 sw.verticalScrollTo(verticalScrollBar.position); 6288 }); 6289 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 6290 horizontalScrollBar.setPosition(event.intValue); 6291 sw.horizontalScrollTo(horizontalScrollBar.position); 6292 }); 6293 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 6294 verticalScrollBar.setPosition(event.intValue); 6295 sw.verticalScrollTo(verticalScrollBar.position); 6296 }); 6297 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 6298 horizontalScrollBar.setPosition(event.intValue); 6299 sw.horizontalScrollTo(horizontalScrollBar.position); 6300 }); 6301 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 6302 verticalScrollBar.setPosition(event.intValue); 6303 }); 6304 } 6305 6306 // this is supposed to be basically invisible... 6307 override int minWidth() { return sw.minWidth; } 6308 override int minHeight() { return sw.minHeight; } 6309 override int maxWidth() { return sw.maxWidth; } 6310 override int maxHeight() { return sw.maxHeight; } 6311 override int widthStretchiness() { return sw.widthStretchiness; } 6312 override int heightStretchiness() { return sw.heightStretchiness; } 6313 override int marginLeft() { return sw.marginLeft; } 6314 override int marginRight() { return sw.marginRight; } 6315 override int marginTop() { return sw.marginTop; } 6316 override int marginBottom() { return sw.marginBottom; } 6317 override int paddingLeft() { return sw.paddingLeft; } 6318 override int paddingRight() { return sw.paddingRight; } 6319 override int paddingTop() { return sw.paddingTop; } 6320 override int paddingBottom() { return sw.paddingBottom; } 6321 override void focus() { sw.focus(); } 6322 6323 6324 override void recomputeChildLayout() { 6325 // The stupid thing needs to calculate if a scroll bar is needed... 6326 recomputeChildLayoutHelper(); 6327 // then running it again will position things correctly if the bar is NOT needed 6328 recomputeChildLayoutHelper(); 6329 6330 // this sucks but meh it barely works 6331 } 6332 6333 private void recomputeChildLayoutHelper() { 6334 if(sw is null) return; 6335 6336 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 6337 if(horizontalScrollBar && verticalScrollBar) { 6338 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 6339 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 6340 horizontalScrollBar.x = 0; 6341 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 6342 6343 verticalScrollBar.width = verticalScrollBar.minWidth(); 6344 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 6345 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 6346 verticalScrollBar.y = 0 + 2; 6347 6348 sw.x = 0; 6349 sw.y = 0; 6350 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 6351 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 6352 6353 if(sw.contentWidth_ <= this.width) 6354 sw.scrollOrigin_.x = 0; 6355 if(sw.contentHeight_ <= this.height) 6356 sw.scrollOrigin_.y = 0; 6357 6358 horizontalScrollBar.recomputeChildLayout(); 6359 verticalScrollBar.recomputeChildLayout(); 6360 sw.recomputeChildLayout(); 6361 } 6362 6363 if(sw.contentWidth_ <= this.width) 6364 sw.scrollOrigin_.x = 0; 6365 if(sw.contentHeight_ <= this.height) 6366 sw.scrollOrigin_.y = 0; 6367 6368 if(sw.showingHorizontalScroll()) 6369 horizontalScrollBar.showing(true, false); 6370 else 6371 horizontalScrollBar.showing(false, false); 6372 if(sw.showingVerticalScroll()) 6373 verticalScrollBar.showing(true, false); 6374 else 6375 verticalScrollBar.showing(false, false); 6376 6377 verticalScrollBar.setViewableArea(sw.viewportHeight()); 6378 verticalScrollBar.setMax(sw.contentHeight); 6379 verticalScrollBar.setPosition(sw.scrollOrigin.y); 6380 6381 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 6382 horizontalScrollBar.setMax(sw.contentWidth); 6383 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 6384 } 6385 } 6386 6387 /* 6388 class ScrollableClientWidget : Widget { 6389 this(Widget parent) { 6390 super(parent); 6391 } 6392 override void paint(WidgetPainter p) { 6393 parent.paint(p); 6394 } 6395 } 6396 */ 6397 6398 /++ 6399 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. 6400 +/ 6401 abstract class Slider : Widget { 6402 this(int min, int max, int step, Widget parent) { 6403 min_ = min; 6404 max_ = max; 6405 step_ = step; 6406 page_ = step; 6407 super(parent); 6408 } 6409 6410 private int min_; 6411 private int max_; 6412 private int step_; 6413 private int position_; 6414 private int page_; 6415 6416 // selection start and selection end 6417 // tics 6418 // tooltip? 6419 // some way to see and just type the value 6420 // win32 buddy controls are labels 6421 6422 /// 6423 void setMin(int a) { 6424 min_ = a; 6425 version(custom_widgets) 6426 redraw(); 6427 version(win32_widgets) 6428 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 6429 } 6430 /// 6431 int min() { 6432 return min_; 6433 } 6434 /// 6435 void setMax(int a) { 6436 max_ = a; 6437 version(custom_widgets) 6438 redraw(); 6439 version(win32_widgets) 6440 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 6441 } 6442 /// 6443 int max() { 6444 return max_; 6445 } 6446 /// 6447 void setPosition(int a) { 6448 if(a > max) 6449 a = max; 6450 if(a < min) 6451 a = min; 6452 position_ = a; 6453 version(custom_widgets) 6454 setPositionCustom(a); 6455 6456 version(win32_widgets) 6457 setPositionWindows(a); 6458 } 6459 version(win32_widgets) { 6460 protected abstract void setPositionWindows(int a); 6461 } 6462 6463 protected abstract int win32direction(); 6464 6465 /++ 6466 Alias for [position] for better compatibility with generic code. 6467 6468 History: 6469 Added October 5, 2021 6470 +/ 6471 @property int value() { 6472 return position; 6473 } 6474 6475 /// 6476 int position() { 6477 return position_; 6478 } 6479 /// 6480 void setStep(int a) { 6481 step_ = a; 6482 version(win32_widgets) 6483 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 6484 } 6485 /// 6486 int step() { 6487 return step_; 6488 } 6489 /// 6490 void setPageSize(int a) { 6491 page_ = a; 6492 version(win32_widgets) 6493 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 6494 } 6495 /// 6496 int pageSize() { 6497 return page_; 6498 } 6499 6500 private void notify() { 6501 auto event = new ChangeEvent!int(this, &this.position); 6502 event.dispatch(); 6503 } 6504 6505 version(win32_widgets) 6506 void win32Setup(int style) { 6507 createWin32Window(this, TRACKBAR_CLASS, "", 6508 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 6509 6510 // the trackbar sends the same messages as scroll, which 6511 // our other layer sends as these... just gonna translate 6512 // here 6513 this.addDirectEventListener("scrolltoposition", (Event event) { 6514 event.stopPropagation(); 6515 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 6516 notify(); 6517 }); 6518 this.addDirectEventListener("scrolltonextline", (Event event) { 6519 event.stopPropagation(); 6520 this.setPosition(this.position + this.step_ * this.win32direction); 6521 notify(); 6522 }); 6523 this.addDirectEventListener("scrolltopreviousline", (Event event) { 6524 event.stopPropagation(); 6525 this.setPosition(this.position - this.step_ * this.win32direction); 6526 notify(); 6527 }); 6528 this.addDirectEventListener("scrolltonextpage", (Event event) { 6529 event.stopPropagation(); 6530 this.setPosition(this.position + this.page_ * this.win32direction); 6531 notify(); 6532 }); 6533 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 6534 event.stopPropagation(); 6535 this.setPosition(this.position - this.page_ * this.win32direction); 6536 notify(); 6537 }); 6538 6539 setMin(min_); 6540 setMax(max_); 6541 setStep(step_); 6542 setPageSize(page_); 6543 } 6544 6545 version(custom_widgets) { 6546 protected MouseTrackingWidget thumb; 6547 6548 protected abstract void setPositionCustom(int a); 6549 6550 override void defaultEventHandler_keydown(KeyDownEvent event) { 6551 switch(event.key) { 6552 case Key.Up: 6553 case Key.Right: 6554 setPosition(position() - step() * win32direction); 6555 changed(); 6556 break; 6557 case Key.Down: 6558 case Key.Left: 6559 setPosition(position() + step() * win32direction); 6560 changed(); 6561 break; 6562 case Key.Home: 6563 setPosition(win32direction > 0 ? min() : max()); 6564 changed(); 6565 break; 6566 case Key.End: 6567 setPosition(win32direction > 0 ? max() : min()); 6568 changed(); 6569 break; 6570 case Key.PageUp: 6571 setPosition(position() - pageSize() * win32direction); 6572 changed(); 6573 break; 6574 case Key.PageDown: 6575 setPosition(position() + pageSize() * win32direction); 6576 changed(); 6577 break; 6578 default: 6579 } 6580 super.defaultEventHandler_keydown(event); 6581 } 6582 6583 protected void changed() { 6584 auto ev = new ChangeEvent!int(this, &position); 6585 ev.dispatch(); 6586 } 6587 } 6588 } 6589 6590 /++ 6591 6592 +/ 6593 class VerticalSlider : Slider { 6594 this(int min, int max, int step, Widget parent) { 6595 version(custom_widgets) 6596 initialize(); 6597 6598 super(min, max, step, parent); 6599 6600 version(win32_widgets) 6601 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 6602 } 6603 6604 protected override int win32direction() { 6605 return -1; 6606 } 6607 6608 version(win32_widgets) 6609 protected override void setPositionWindows(int a) { 6610 // the windows thing makes the top 0 and i don't like that. 6611 SendMessage(hwnd, TBM_SETPOS, true, max - a); 6612 } 6613 6614 version(custom_widgets) 6615 private void initialize() { 6616 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 6617 6618 thumb.tabStop = false; 6619 6620 thumb.thumbWidth = width; 6621 thumb.thumbHeight = scaleWithDpi(16); 6622 6623 thumb.addEventListener(EventType.change, () { 6624 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 6625 sx = max - sx; 6626 //informProgramThatUserChangedPosition(sx); 6627 6628 position_ = sx; 6629 6630 changed(); 6631 }); 6632 } 6633 6634 version(custom_widgets) 6635 override void recomputeChildLayout() { 6636 thumb.thumbWidth = this.width; 6637 super.recomputeChildLayout(); 6638 setPositionCustom(position_); 6639 } 6640 6641 version(custom_widgets) 6642 protected override void setPositionCustom(int a) { 6643 if(max()) 6644 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 6645 redraw(); 6646 } 6647 } 6648 6649 /++ 6650 6651 +/ 6652 class HorizontalSlider : Slider { 6653 this(int min, int max, int step, Widget parent) { 6654 version(custom_widgets) 6655 initialize(); 6656 6657 super(min, max, step, parent); 6658 6659 version(win32_widgets) 6660 win32Setup(TBS_HORZ); 6661 } 6662 6663 version(win32_widgets) 6664 protected override void setPositionWindows(int a) { 6665 SendMessage(hwnd, TBM_SETPOS, true, a); 6666 } 6667 6668 protected override int win32direction() { 6669 return 1; 6670 } 6671 6672 version(custom_widgets) 6673 private void initialize() { 6674 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 6675 6676 thumb.tabStop = false; 6677 6678 thumb.thumbWidth = scaleWithDpi(16); 6679 thumb.thumbHeight = height; 6680 6681 thumb.addEventListener(EventType.change, () { 6682 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6683 //informProgramThatUserChangedPosition(sx); 6684 6685 position_ = sx; 6686 6687 changed(); 6688 }); 6689 } 6690 6691 version(custom_widgets) 6692 override void recomputeChildLayout() { 6693 thumb.thumbHeight = this.height; 6694 super.recomputeChildLayout(); 6695 setPositionCustom(position_); 6696 } 6697 6698 version(custom_widgets) 6699 protected override void setPositionCustom(int a) { 6700 if(max()) 6701 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6702 redraw(); 6703 } 6704 } 6705 6706 6707 /// 6708 abstract class ScrollbarBase : Widget { 6709 /// 6710 this(Widget parent) { 6711 super(parent); 6712 tabStop = false; 6713 step_ = scaleWithDpi(16); 6714 } 6715 6716 private int viewableArea_; 6717 private int max_; 6718 private int step_;// = 16; 6719 private int position_; 6720 6721 /// 6722 bool atEnd() { 6723 return position_ + viewableArea_ >= max_; 6724 } 6725 6726 /// 6727 bool atStart() { 6728 return position_ == 0; 6729 } 6730 6731 /// 6732 void setViewableArea(int a) { 6733 viewableArea_ = a; 6734 version(custom_widgets) 6735 redraw(); 6736 } 6737 /// 6738 void setMax(int a) { 6739 max_ = a; 6740 version(custom_widgets) 6741 redraw(); 6742 } 6743 /// 6744 int max() { 6745 return max_; 6746 } 6747 /// 6748 void setPosition(int a) { 6749 auto logicalMax = max_ - viewableArea_; 6750 if(a == int.max) 6751 a = logicalMax; 6752 6753 if(a > logicalMax) 6754 a = logicalMax; 6755 if(a < 0) 6756 a = 0; 6757 6758 position_ = a; 6759 6760 version(custom_widgets) 6761 redraw(); 6762 } 6763 /// 6764 int position() { 6765 return position_; 6766 } 6767 /// 6768 void setStep(int a) { 6769 step_ = a; 6770 } 6771 /// 6772 int step() { 6773 return step_; 6774 } 6775 6776 // FIXME: remove this.... maybe 6777 /+ 6778 protected void informProgramThatUserChangedPosition(int n) { 6779 position_ = n; 6780 auto evt = new Event(EventType.change, this); 6781 evt.intValue = n; 6782 evt.dispatch(); 6783 } 6784 +/ 6785 6786 version(custom_widgets) { 6787 enum MIN_THUMB_SIZE = 8; 6788 6789 abstract protected int getBarDim(); 6790 int thumbSize() { 6791 if(viewableArea_ >= max_ || max_ == 0) 6792 return getBarDim(); 6793 6794 int res = viewableArea_ * getBarDim() / max_; 6795 6796 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6797 res = scaleWithDpi(MIN_THUMB_SIZE); 6798 6799 return res; 6800 } 6801 6802 int thumbPosition() { 6803 /* 6804 viewableArea_ is the viewport height/width 6805 position_ is where we are 6806 */ 6807 //if(position_ + viewableArea_ >= max_) 6808 //return getBarDim - thumbSize; 6809 6810 auto maximumPossibleValue = getBarDim() - thumbSize; 6811 auto maximiumLogicalValue = max_ - viewableArea_; 6812 6813 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6814 6815 return p; 6816 } 6817 } 6818 } 6819 6820 //public import mgt; 6821 6822 /++ 6823 A mouse tracking widget is one that follows the mouse when dragged inside it. 6824 6825 Concrete subclasses may include a scrollbar thumb and a volume control. 6826 +/ 6827 //version(custom_widgets) 6828 class MouseTrackingWidget : Widget { 6829 6830 /// 6831 int positionX() { return positionX_; } 6832 /// 6833 int positionY() { return positionY_; } 6834 6835 /// 6836 void positionX(int p) { positionX_ = p; } 6837 /// 6838 void positionY(int p) { positionY_ = p; } 6839 6840 private int positionX_; 6841 private int positionY_; 6842 6843 /// 6844 enum Orientation { 6845 horizontal, /// 6846 vertical, /// 6847 twoDimensional, /// 6848 } 6849 6850 private int thumbWidth_; 6851 private int thumbHeight_; 6852 6853 /// 6854 int thumbWidth() { return thumbWidth_; } 6855 /// 6856 int thumbHeight() { return thumbHeight_; } 6857 /// 6858 int thumbWidth(int a) { return thumbWidth_ = a; } 6859 /// 6860 int thumbHeight(int a) { return thumbHeight_ = a; } 6861 6862 private bool dragging; 6863 private bool hovering; 6864 private int startMouseX, startMouseY; 6865 6866 /// 6867 this(Orientation orientation, Widget parent) { 6868 super(parent); 6869 6870 //assert(parentWindow !is null); 6871 6872 addEventListener((MouseDownEvent event) { 6873 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6874 dragging = true; 6875 startMouseX = event.clientX - positionX; 6876 startMouseY = event.clientY - positionY; 6877 parentWindow.captureMouse(this); 6878 } else { 6879 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6880 positionX = event.clientX - thumbWidth / 2; 6881 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6882 positionY = event.clientY - thumbHeight / 2; 6883 6884 if(positionX + thumbWidth > this.width) 6885 positionX = this.width - thumbWidth; 6886 if(positionY + thumbHeight > this.height) 6887 positionY = this.height - thumbHeight; 6888 6889 if(positionX < 0) 6890 positionX = 0; 6891 if(positionY < 0) 6892 positionY = 0; 6893 6894 6895 // this.emit!(ChangeEvent!void)(); 6896 auto evt = new Event(EventType.change, this); 6897 evt.sendDirectly(); 6898 6899 redraw(); 6900 6901 } 6902 }); 6903 6904 addEventListener(EventType.mouseup, (Event event) { 6905 dragging = false; 6906 parentWindow.releaseMouseCapture(); 6907 }); 6908 6909 addEventListener(EventType.mouseout, (Event event) { 6910 if(!hovering) 6911 return; 6912 hovering = false; 6913 redraw(); 6914 }); 6915 6916 int lpx, lpy; 6917 6918 addEventListener((MouseMoveEvent event) { 6919 auto oh = hovering; 6920 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6921 hovering = true; 6922 } else { 6923 hovering = false; 6924 } 6925 if(!dragging) { 6926 if(hovering != oh) 6927 redraw(); 6928 return; 6929 } 6930 6931 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6932 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6933 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6934 positionY = event.clientY - startMouseY; 6935 6936 if(positionX + thumbWidth > this.width) 6937 positionX = this.width - thumbWidth; 6938 if(positionY + thumbHeight > this.height) 6939 positionY = this.height - thumbHeight; 6940 6941 if(positionX < 0) 6942 positionX = 0; 6943 if(positionY < 0) 6944 positionY = 0; 6945 6946 if(positionX != lpx || positionY != lpy) { 6947 lpx = positionX; 6948 lpy = positionY; 6949 6950 auto evt = new Event(EventType.change, this); 6951 evt.sendDirectly(); 6952 } 6953 6954 redraw(); 6955 }); 6956 } 6957 6958 version(custom_widgets) 6959 override void paint(WidgetPainter painter) { 6960 auto cs = getComputedStyle(); 6961 auto c = darken(cs.windowBackgroundColor, 0.2); 6962 painter.outlineColor = c; 6963 painter.fillColor = c; 6964 painter.drawRectangle(Point(0, 0), this.width, this.height); 6965 6966 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6967 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6968 } 6969 } 6970 6971 //version(custom_widgets) 6972 //private 6973 class HorizontalScrollbar : ScrollbarBase { 6974 6975 version(custom_widgets) { 6976 private MouseTrackingWidget thumb; 6977 6978 override int getBarDim() { 6979 return thumb.width; 6980 } 6981 } 6982 6983 override void setViewableArea(int a) { 6984 super.setViewableArea(a); 6985 6986 version(win32_widgets) { 6987 SCROLLINFO info; 6988 info.cbSize = info.sizeof; 6989 info.nPage = a + 1; 6990 info.fMask = SIF_PAGE; 6991 SetScrollInfo(hwnd, SB_CTL, &info, true); 6992 } else version(custom_widgets) { 6993 thumb.positionX = thumbPosition; 6994 thumb.thumbWidth = thumbSize; 6995 thumb.redraw(); 6996 } else static assert(0); 6997 6998 } 6999 7000 override void setMax(int a) { 7001 super.setMax(a); 7002 version(win32_widgets) { 7003 SCROLLINFO info; 7004 info.cbSize = info.sizeof; 7005 info.nMin = 0; 7006 info.nMax = max; 7007 info.fMask = SIF_RANGE; 7008 SetScrollInfo(hwnd, SB_CTL, &info, true); 7009 } else version(custom_widgets) { 7010 thumb.positionX = thumbPosition; 7011 thumb.thumbWidth = thumbSize; 7012 thumb.redraw(); 7013 } 7014 } 7015 7016 override void setPosition(int a) { 7017 super.setPosition(a); 7018 version(win32_widgets) { 7019 SCROLLINFO info; 7020 info.cbSize = info.sizeof; 7021 info.fMask = SIF_POS; 7022 info.nPos = position; 7023 SetScrollInfo(hwnd, SB_CTL, &info, true); 7024 } else version(custom_widgets) { 7025 thumb.positionX = thumbPosition(); 7026 thumb.thumbWidth = thumbSize; 7027 thumb.redraw(); 7028 } else static assert(0); 7029 } 7030 7031 this(Widget parent) { 7032 super(parent); 7033 7034 version(win32_widgets) { 7035 createWin32Window(this, "Scrollbar"w, "", 7036 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 7037 } else version(custom_widgets) { 7038 auto vl = new HorizontalLayout(this); 7039 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 7040 leftButton.setClickRepeat(scrollClickRepeatInterval); 7041 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 7042 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 7043 rightButton.setClickRepeat(scrollClickRepeatInterval); 7044 7045 leftButton.tabStop = false; 7046 rightButton.tabStop = false; 7047 thumb.tabStop = false; 7048 7049 leftButton.addEventListener(EventType.triggered, () { 7050 this.emitCommand!"scrolltopreviousline"(); 7051 //informProgramThatUserChangedPosition(position - step()); 7052 }); 7053 rightButton.addEventListener(EventType.triggered, () { 7054 this.emitCommand!"scrolltonextline"(); 7055 //informProgramThatUserChangedPosition(position + step()); 7056 }); 7057 7058 thumb.thumbWidth = this.minWidth; 7059 thumb.thumbHeight = scaleWithDpi(16); 7060 7061 thumb.addEventListener(EventType.change, () { 7062 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 7063 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 7064 7065 //informProgramThatUserChangedPosition(sx); 7066 7067 auto ev = new ScrollToPositionEvent(this, sx); 7068 ev.dispatch(); 7069 }); 7070 } 7071 } 7072 7073 override int minHeight() { return scaleWithDpi(16); } 7074 override int maxHeight() { return scaleWithDpi(16); } 7075 override int minWidth() { return scaleWithDpi(48); } 7076 } 7077 7078 class ScrollToPositionEvent : Event { 7079 enum EventString = "scrolltoposition"; 7080 7081 this(Widget target, int value) { 7082 this.value = value; 7083 super(EventString, target); 7084 } 7085 7086 immutable int value; 7087 7088 override @property int intValue() { 7089 return value; 7090 } 7091 } 7092 7093 //version(custom_widgets) 7094 //private 7095 class VerticalScrollbar : ScrollbarBase { 7096 7097 version(custom_widgets) { 7098 override int getBarDim() { 7099 return thumb.height; 7100 } 7101 7102 private MouseTrackingWidget thumb; 7103 } 7104 7105 override void setViewableArea(int a) { 7106 super.setViewableArea(a); 7107 7108 version(win32_widgets) { 7109 SCROLLINFO info; 7110 info.cbSize = info.sizeof; 7111 info.nPage = a + 1; 7112 info.fMask = SIF_PAGE; 7113 SetScrollInfo(hwnd, SB_CTL, &info, true); 7114 } else version(custom_widgets) { 7115 thumb.positionY = thumbPosition; 7116 thumb.thumbHeight = thumbSize; 7117 thumb.redraw(); 7118 } else static assert(0); 7119 7120 } 7121 7122 override void setMax(int a) { 7123 super.setMax(a); 7124 version(win32_widgets) { 7125 SCROLLINFO info; 7126 info.cbSize = info.sizeof; 7127 info.nMin = 0; 7128 info.nMax = max; 7129 info.fMask = SIF_RANGE; 7130 SetScrollInfo(hwnd, SB_CTL, &info, true); 7131 } else version(custom_widgets) { 7132 thumb.positionY = thumbPosition; 7133 thumb.thumbHeight = thumbSize; 7134 thumb.redraw(); 7135 } 7136 } 7137 7138 override void setPosition(int a) { 7139 super.setPosition(a); 7140 version(win32_widgets) { 7141 SCROLLINFO info; 7142 info.cbSize = info.sizeof; 7143 info.fMask = SIF_POS; 7144 info.nPos = position; 7145 SetScrollInfo(hwnd, SB_CTL, &info, true); 7146 } else version(custom_widgets) { 7147 thumb.positionY = thumbPosition; 7148 thumb.thumbHeight = thumbSize; 7149 thumb.redraw(); 7150 } else static assert(0); 7151 } 7152 7153 this(Widget parent) { 7154 super(parent); 7155 7156 version(win32_widgets) { 7157 createWin32Window(this, "Scrollbar"w, "", 7158 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 7159 } else version(custom_widgets) { 7160 auto vl = new VerticalLayout(this); 7161 auto upButton = new ArrowButton(ArrowDirection.up, vl); 7162 upButton.setClickRepeat(scrollClickRepeatInterval); 7163 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 7164 auto downButton = new ArrowButton(ArrowDirection.down, vl); 7165 downButton.setClickRepeat(scrollClickRepeatInterval); 7166 7167 upButton.addEventListener(EventType.triggered, () { 7168 this.emitCommand!"scrolltopreviousline"(); 7169 //informProgramThatUserChangedPosition(position - step()); 7170 }); 7171 downButton.addEventListener(EventType.triggered, () { 7172 this.emitCommand!"scrolltonextline"(); 7173 //informProgramThatUserChangedPosition(position + step()); 7174 }); 7175 7176 thumb.thumbWidth = this.minWidth; 7177 thumb.thumbHeight = scaleWithDpi(16); 7178 7179 thumb.addEventListener(EventType.change, () { 7180 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 7181 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 7182 7183 auto ev = new ScrollToPositionEvent(this, sy); 7184 ev.dispatch(); 7185 7186 //informProgramThatUserChangedPosition(sy); 7187 }); 7188 7189 upButton.tabStop = false; 7190 downButton.tabStop = false; 7191 thumb.tabStop = false; 7192 } 7193 } 7194 7195 override int minWidth() { return scaleWithDpi(16); } 7196 override int maxWidth() { return scaleWithDpi(16); } 7197 override int minHeight() { return scaleWithDpi(48); } 7198 } 7199 7200 7201 /++ 7202 EXPERIMENTAL 7203 7204 A widget specialized for being a container for other widgets. 7205 7206 History: 7207 Added May 29, 2021. Not stabilized at this time. 7208 +/ 7209 class WidgetContainer : Widget { 7210 this(Widget parent) { 7211 tabStop = false; 7212 super(parent); 7213 } 7214 7215 override int maxHeight() { 7216 if(this.children.length == 1) { 7217 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 7218 } else { 7219 return int.max; 7220 } 7221 } 7222 7223 override int maxWidth() { 7224 if(this.children.length == 1) { 7225 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 7226 } else { 7227 return int.max; 7228 } 7229 } 7230 7231 /+ 7232 7233 override int minHeight() { 7234 int largest = 0; 7235 int margins = 0; 7236 int lastMargin = 0; 7237 foreach(child; children) { 7238 auto mh = child.minHeight(); 7239 if(mh > largest) 7240 largest = mh; 7241 margins += mymax(lastMargin, child.marginTop()); 7242 lastMargin = child.marginBottom(); 7243 } 7244 return largest + margins; 7245 } 7246 7247 override int maxHeight() { 7248 int largest = 0; 7249 int margins = 0; 7250 int lastMargin = 0; 7251 foreach(child; children) { 7252 auto mh = child.maxHeight(); 7253 if(mh == int.max) 7254 return int.max; 7255 if(mh > largest) 7256 largest = mh; 7257 margins += mymax(lastMargin, child.marginTop()); 7258 lastMargin = child.marginBottom(); 7259 } 7260 return largest + margins; 7261 } 7262 7263 override int minWidth() { 7264 int min; 7265 foreach(child; children) { 7266 auto cm = child.minWidth; 7267 if(cm > min) 7268 min = cm; 7269 } 7270 return min + paddingLeft + paddingRight; 7271 } 7272 7273 override int minHeight() { 7274 int min; 7275 foreach(child; children) { 7276 auto cm = child.minHeight; 7277 if(cm > min) 7278 min = cm; 7279 } 7280 return min + paddingTop + paddingBottom; 7281 } 7282 7283 override int maxHeight() { 7284 int largest = 0; 7285 int margins = 0; 7286 int lastMargin = 0; 7287 foreach(child; children) { 7288 auto mh = child.maxHeight(); 7289 if(mh == int.max) 7290 return int.max; 7291 if(mh > largest) 7292 largest = mh; 7293 margins += mymax(lastMargin, child.marginTop()); 7294 lastMargin = child.marginBottom(); 7295 } 7296 return largest + margins; 7297 } 7298 7299 override int heightStretchiness() { 7300 int max; 7301 foreach(child; children) { 7302 auto c = child.heightStretchiness; 7303 if(c > max) 7304 max = c; 7305 } 7306 return max; 7307 } 7308 7309 override int marginTop() { 7310 if(this.children.length) 7311 return this.children[0].marginTop; 7312 return 0; 7313 } 7314 +/ 7315 } 7316 7317 /// 7318 abstract class Layout : Widget { 7319 this(Widget parent) { 7320 tabStop = false; 7321 super(parent); 7322 } 7323 } 7324 7325 /++ 7326 Makes all children minimum width and height, placing them down 7327 left to right, top to bottom. 7328 7329 Useful if you want to make a list of buttons that automatically 7330 wrap to a new line when necessary. 7331 +/ 7332 class InlineBlockLayout : Layout { 7333 /// 7334 this(Widget parent) { super(parent); } 7335 7336 override void recomputeChildLayout() { 7337 registerMovement(); 7338 7339 int x = this.paddingLeft, y = this.paddingTop; 7340 7341 int lineHeight; 7342 int previousMargin = 0; 7343 int previousMarginBottom = 0; 7344 7345 foreach(child; children) { 7346 if(child.hidden) 7347 continue; 7348 if(cast(FixedPosition) child) { 7349 child.recomputeChildLayout(); 7350 continue; 7351 } 7352 child.width = child.flexBasisWidth(); 7353 if(child.width == 0) 7354 child.width = child.minWidth(); 7355 if(child.width == 0) 7356 child.width = 32; 7357 7358 child.height = child.flexBasisHeight(); 7359 if(child.height == 0) 7360 child.height = child.minHeight(); 7361 if(child.height == 0) 7362 child.height = 32; 7363 7364 if(x + child.width + paddingRight > this.width) { 7365 x = this.paddingLeft; 7366 y += lineHeight; 7367 lineHeight = 0; 7368 previousMargin = 0; 7369 previousMarginBottom = 0; 7370 } 7371 7372 auto margin = child.marginLeft; 7373 if(previousMargin > margin) 7374 margin = previousMargin; 7375 7376 x += margin; 7377 7378 child.x = x; 7379 child.y = y; 7380 7381 int marginTopApplied; 7382 if(child.marginTop > previousMarginBottom) { 7383 child.y += child.marginTop; 7384 marginTopApplied = child.marginTop; 7385 } 7386 7387 x += child.width; 7388 previousMargin = child.marginRight; 7389 7390 if(child.marginBottom > previousMarginBottom) 7391 previousMarginBottom = child.marginBottom; 7392 7393 auto h = child.height + previousMarginBottom + marginTopApplied; 7394 if(h > lineHeight) 7395 lineHeight = h; 7396 7397 child.recomputeChildLayout(); 7398 } 7399 7400 } 7401 7402 override int minWidth() { 7403 int min; 7404 foreach(child; children) { 7405 auto cm = child.minWidth; 7406 if(cm > min) 7407 min = cm; 7408 } 7409 return min + paddingLeft + paddingRight; 7410 } 7411 7412 override int minHeight() { 7413 int min; 7414 foreach(child; children) { 7415 auto cm = child.minHeight; 7416 if(cm > min) 7417 min = cm; 7418 } 7419 return min + paddingTop + paddingBottom; 7420 } 7421 } 7422 7423 /++ 7424 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 7425 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 7426 the [TabWidget] will automatically change pages of child widgets. 7427 7428 This allows you to react to it however you see fit rather than having to 7429 be tied to just the new sets of child widgets. 7430 7431 It sends the message in the form of `this.emitCommand!"changetab"();`. 7432 7433 History: 7434 Added December 24, 2021 (dub v10.5) 7435 +/ 7436 class TabMessageWidget : Widget { 7437 7438 protected void tabIndexClicked(int item) { 7439 this.emitCommand!"changetab"(); 7440 } 7441 7442 /++ 7443 Adds the a new tab to the control with the given title. 7444 7445 Returns: 7446 The index of the newly added tab. You will need to know 7447 this index to refer to it later and to know which tab to 7448 change to when you get a changetab message. 7449 +/ 7450 int addTab(string title, int pos = int.max) { 7451 version(win32_widgets) { 7452 TCITEM item; 7453 item.mask = TCIF_TEXT; 7454 WCharzBuffer buf = WCharzBuffer(title); 7455 item.pszText = buf.ptr; 7456 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 7457 } else version(custom_widgets) { 7458 if(pos >= tabs.length) { 7459 tabs ~= title; 7460 redraw(); 7461 return cast(int) tabs.length - 1; 7462 } else if(pos <= 0) { 7463 tabs = title ~ tabs; 7464 redraw(); 7465 return 0; 7466 } else { 7467 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 7468 redraw(); 7469 return pos; 7470 } 7471 } 7472 } 7473 7474 override void addChild(Widget child, int pos = int.max) { 7475 if(container) 7476 container.addChild(child, pos); 7477 else 7478 super.addChild(child, pos); 7479 } 7480 7481 protected Widget makeContainer() { 7482 return new Widget(this); 7483 } 7484 7485 private Widget container; 7486 7487 override void recomputeChildLayout() { 7488 version(win32_widgets) { 7489 this.registerMovement(); 7490 7491 RECT rect; 7492 GetWindowRect(hwnd, &rect); 7493 7494 auto left = rect.left; 7495 auto top = rect.top; 7496 7497 TabCtrl_AdjustRect(hwnd, false, &rect); 7498 foreach(child; children) { 7499 if(!child.showing) continue; 7500 child.x = rect.left - left; 7501 child.y = rect.top - top; 7502 child.width = rect.right - rect.left; 7503 child.height = rect.bottom - rect.top; 7504 child.recomputeChildLayout(); 7505 } 7506 } else version(custom_widgets) { 7507 this.registerMovement(); 7508 foreach(child; children) { 7509 if(!child.showing) continue; 7510 child.x = 2; 7511 child.y = tabBarHeight + 2; // for the border 7512 child.width = width - 4; // for the border 7513 child.height = height - tabBarHeight - 2 - 2; // for the border 7514 child.recomputeChildLayout(); 7515 } 7516 } else static assert(0); 7517 } 7518 7519 version(custom_widgets) 7520 string[] tabs; 7521 7522 this(Widget parent) { 7523 super(parent); 7524 7525 tabStop = false; 7526 7527 version(win32_widgets) { 7528 createWin32Window(this, WC_TABCONTROL, "", 0); 7529 } else version(custom_widgets) { 7530 addEventListener((ClickEvent event) { 7531 if(event.target !is this) 7532 return; 7533 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 7534 auto t = (event.clientX / tabWidth); 7535 if(t >= 0 && t < tabs.length) { 7536 currentTab_ = t; 7537 tabIndexClicked(t); 7538 redraw(); 7539 } 7540 } 7541 }); 7542 } else static assert(0); 7543 7544 this.container = makeContainer(); 7545 } 7546 7547 override int marginTop() { return 4; } 7548 override int paddingBottom() { return 4; } 7549 7550 override int minHeight() { 7551 int max = 0; 7552 foreach(child; children) 7553 max = mymax(child.minHeight, max); 7554 7555 7556 version(win32_widgets) { 7557 RECT rect; 7558 rect.right = this.width; 7559 rect.bottom = max; 7560 TabCtrl_AdjustRect(hwnd, true, &rect); 7561 7562 max = rect.bottom; 7563 } else { 7564 max += defaultLineHeight + 4; 7565 } 7566 7567 7568 return max; 7569 } 7570 7571 version(win32_widgets) 7572 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 7573 switch(code) { 7574 case TCN_SELCHANGE: 7575 auto sel = TabCtrl_GetCurSel(hwnd); 7576 tabIndexClicked(sel); 7577 break; 7578 default: 7579 } 7580 return 0; 7581 } 7582 7583 version(custom_widgets) { 7584 private int currentTab_; 7585 private int tabBarHeight() { return defaultLineHeight; } 7586 int tabWidth() { return scaleWithDpi(80); } 7587 } 7588 7589 version(win32_widgets) 7590 override void paint(WidgetPainter painter) {} 7591 7592 version(custom_widgets) 7593 override void paint(WidgetPainter painter) { 7594 auto cs = getComputedStyle(); 7595 7596 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 7597 7598 int posX = 0; 7599 foreach(idx, title; tabs) { 7600 auto isCurrent = idx == getCurrentTab(); 7601 7602 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 7603 7604 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 7605 painter.outlineColor = cs.foregroundColor; 7606 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 7607 7608 if(isCurrent) { 7609 painter.outlineColor = cs.windowBackgroundColor; 7610 painter.fillColor = Color.transparent; 7611 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 7612 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 7613 7614 painter.outlineColor = Color.white; 7615 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 7616 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 7617 painter.outlineColor = cs.activeTabColor; 7618 painter.drawPixel(Point(posX, tabBarHeight - 1)); 7619 } 7620 7621 posX += tabWidth - 2; 7622 } 7623 } 7624 7625 /// 7626 @scriptable 7627 void setCurrentTab(int item) { 7628 version(win32_widgets) 7629 TabCtrl_SetCurSel(hwnd, item); 7630 else version(custom_widgets) 7631 currentTab_ = item; 7632 else static assert(0); 7633 7634 tabIndexClicked(item); 7635 } 7636 7637 /// 7638 @scriptable 7639 int getCurrentTab() { 7640 version(win32_widgets) 7641 return TabCtrl_GetCurSel(hwnd); 7642 else version(custom_widgets) 7643 return currentTab_; // FIXME 7644 else static assert(0); 7645 } 7646 7647 /// 7648 @scriptable 7649 void removeTab(int item) { 7650 if(item && item == getCurrentTab()) 7651 setCurrentTab(item - 1); 7652 7653 version(win32_widgets) { 7654 TabCtrl_DeleteItem(hwnd, item); 7655 } 7656 7657 for(int a = item; a < children.length - 1; a++) 7658 this._children[a] = this._children[a + 1]; 7659 this._children = this._children[0 .. $-1]; 7660 } 7661 7662 } 7663 7664 7665 /++ 7666 A tab widget is a set of clickable tab buttons followed by a content area. 7667 7668 7669 Tabs can change existing content or can be new pages. 7670 7671 When the user picks a different tab, a `change` message is generated. 7672 +/ 7673 class TabWidget : TabMessageWidget { 7674 this(Widget parent) { 7675 super(parent); 7676 } 7677 7678 override protected Widget makeContainer() { 7679 return null; 7680 } 7681 7682 override void addChild(Widget child, int pos = int.max) { 7683 if(auto twp = cast(TabWidgetPage) child) { 7684 Widget.addChild(child, pos); 7685 if(pos == int.max) 7686 pos = cast(int) this.children.length - 1; 7687 7688 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7689 7690 if(pos != getCurrentTab) { 7691 child.showing = false; 7692 } 7693 } else { 7694 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7695 } 7696 } 7697 7698 // FIXME: add tab icons at some point, Windows supports them 7699 /++ 7700 Adds a page and its associated tab with the given label to the widget. 7701 7702 Returns: 7703 The added page object, to which you can add other widgets. 7704 +/ 7705 @scriptable 7706 TabWidgetPage addPage(string title) { 7707 return new TabWidgetPage(title, this); 7708 } 7709 7710 /++ 7711 Gets the page at the given tab index, or `null` if the index is bad. 7712 7713 History: 7714 Added December 24, 2021. 7715 +/ 7716 TabWidgetPage getPage(int index) { 7717 if(index < this.children.length) 7718 return null; 7719 return cast(TabWidgetPage) this.children[index]; 7720 } 7721 7722 /++ 7723 While you can still use the addTab from the parent class, 7724 *strongly* recommend you use [addPage] insteaad. 7725 7726 History: 7727 Added December 24, 2021 to fulful the interface 7728 requirement that came from adding [TabMessageWidget]. 7729 7730 You should not use it though since the [addPage] function 7731 is much easier to use here. 7732 +/ 7733 override int addTab(string title, int pos = int.max) { 7734 auto p = addPage(title); 7735 foreach(idx, child; this.children) 7736 if(child is p) 7737 return cast(int) idx; 7738 return -1; 7739 } 7740 7741 protected override void tabIndexClicked(int item) { 7742 foreach(idx, child; children) { 7743 child.showing(false, false); // batch the recalculates for the end 7744 } 7745 7746 foreach(idx, child; children) { 7747 if(idx == item) { 7748 child.showing(true, false); 7749 if(parentWindow) { 7750 auto f = parentWindow.getFirstFocusable(child); 7751 if(f) 7752 f.focus(); 7753 } 7754 recomputeChildLayout(); 7755 } 7756 } 7757 7758 version(win32_widgets) { 7759 InvalidateRect(hwnd, null, true); 7760 } else version(custom_widgets) { 7761 this.redraw(); 7762 } 7763 } 7764 7765 } 7766 7767 /++ 7768 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7769 7770 You add [TabWidgetPage]s to it. 7771 +/ 7772 class PageWidget : Widget { 7773 this(Widget parent) { 7774 super(parent); 7775 } 7776 7777 override int minHeight() { 7778 int max = 0; 7779 foreach(child; children) 7780 max = mymax(child.minHeight, max); 7781 7782 return max; 7783 } 7784 7785 7786 override void addChild(Widget child, int pos = int.max) { 7787 if(auto twp = cast(TabWidgetPage) child) { 7788 super.addChild(child, pos); 7789 if(pos == int.max) 7790 pos = cast(int) this.children.length - 1; 7791 7792 if(pos != getCurrentTab) { 7793 child.showing = false; 7794 } 7795 } else { 7796 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7797 } 7798 } 7799 7800 override void recomputeChildLayout() { 7801 this.registerMovement(); 7802 foreach(child; children) { 7803 child.x = 0; 7804 child.y = 0; 7805 child.width = width; 7806 child.height = height; 7807 child.recomputeChildLayout(); 7808 } 7809 } 7810 7811 private int currentTab_; 7812 7813 /// 7814 @scriptable 7815 void setCurrentTab(int item) { 7816 currentTab_ = item; 7817 7818 showOnly(item); 7819 } 7820 7821 /// 7822 @scriptable 7823 int getCurrentTab() { 7824 return currentTab_; 7825 } 7826 7827 /// 7828 @scriptable 7829 void removeTab(int item) { 7830 if(item && item == getCurrentTab()) 7831 setCurrentTab(item - 1); 7832 7833 for(int a = item; a < children.length - 1; a++) 7834 this._children[a] = this._children[a + 1]; 7835 this._children = this._children[0 .. $-1]; 7836 } 7837 7838 /// 7839 @scriptable 7840 TabWidgetPage addPage(string title) { 7841 return new TabWidgetPage(title, this); 7842 } 7843 7844 private void showOnly(int item) { 7845 foreach(idx, child; children) 7846 if(idx == item) { 7847 child.show(); 7848 child.queueRecomputeChildLayout(); 7849 } else { 7850 child.hide(); 7851 } 7852 } 7853 } 7854 7855 /++ 7856 7857 +/ 7858 class TabWidgetPage : Widget { 7859 string title; 7860 this(string title, Widget parent) { 7861 this.title = title; 7862 this.tabStop = false; 7863 super(parent); 7864 7865 ///* 7866 version(win32_widgets) { 7867 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7868 } 7869 //*/ 7870 } 7871 7872 override int minHeight() { 7873 int sum = 0; 7874 foreach(child; children) 7875 sum += child.minHeight(); 7876 return sum; 7877 } 7878 } 7879 7880 version(none) 7881 /++ 7882 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7883 7884 I think I need to modify the layout algorithms to support this. 7885 +/ 7886 class CollapsableSidebar : Widget { 7887 7888 } 7889 7890 /// Stacks the widgets vertically, taking all the available width for each child. 7891 class VerticalLayout : Layout { 7892 // most of this is intentionally blank - widget's default is vertical layout right now 7893 /// 7894 this(Widget parent) { super(parent); } 7895 7896 /++ 7897 Sets a max width for the layout so you don't have to subclass. The max width 7898 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7899 7900 History: 7901 Added November 29, 2021 (dub v10.5) 7902 +/ 7903 this(int maxWidth, Widget parent) { 7904 this.mw = maxWidth; 7905 super(parent); 7906 } 7907 7908 private int mw = int.max; 7909 7910 override int maxWidth() { return scaleWithDpi(mw); } 7911 } 7912 7913 /// Stacks the widgets horizontally, taking all the available height for each child. 7914 class HorizontalLayout : Layout { 7915 /// 7916 this(Widget parent) { super(parent); } 7917 7918 /++ 7919 Sets a max height for the layout so you don't have to subclass. The max height 7920 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7921 7922 History: 7923 Added November 29, 2021 (dub v10.5) 7924 +/ 7925 this(int maxHeight, Widget parent) { 7926 this.mh = maxHeight; 7927 super(parent); 7928 } 7929 7930 private int mh = 0; 7931 7932 7933 7934 override void recomputeChildLayout() { 7935 .recomputeChildLayout!"width"(this); 7936 } 7937 7938 override int minHeight() { 7939 int largest = 0; 7940 int margins = 0; 7941 int lastMargin = 0; 7942 foreach(child; children) { 7943 auto mh = child.minHeight(); 7944 if(mh > largest) 7945 largest = mh; 7946 margins += mymax(lastMargin, child.marginTop()); 7947 lastMargin = child.marginBottom(); 7948 } 7949 return largest + margins; 7950 } 7951 7952 override int maxHeight() { 7953 if(mh != 0) 7954 return mymax(minHeight, scaleWithDpi(mh)); 7955 7956 int largest = 0; 7957 int margins = 0; 7958 int lastMargin = 0; 7959 foreach(child; children) { 7960 auto mh = child.maxHeight(); 7961 if(mh == int.max) 7962 return int.max; 7963 if(mh > largest) 7964 largest = mh; 7965 margins += mymax(lastMargin, child.marginTop()); 7966 lastMargin = child.marginBottom(); 7967 } 7968 return largest + margins; 7969 } 7970 7971 override int heightStretchiness() { 7972 int max; 7973 foreach(child; children) { 7974 auto c = child.heightStretchiness; 7975 if(c > max) 7976 max = c; 7977 } 7978 return max; 7979 } 7980 } 7981 7982 version(win32_widgets) 7983 private 7984 extern(Windows) 7985 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7986 Widget* pwin = hwnd in Widget.nativeMapping; 7987 if(pwin is null) 7988 return DefWindowProc(hwnd, message, wparam, lparam); 7989 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7990 if(win is null) 7991 return DefWindowProc(hwnd, message, wparam, lparam); 7992 7993 switch(message) { 7994 case WM_SIZE: 7995 auto width = LOWORD(lparam); 7996 auto height = HIWORD(lparam); 7997 7998 auto hdc = GetDC(hwnd); 7999 auto hdcBmp = CreateCompatibleDC(hdc); 8000 8001 // FIXME: could this be more efficient? it never relinquishes a large bitmap 8002 if(width > win.bmpWidth || height > win.bmpHeight) { 8003 auto oldBuffer = win.buffer; 8004 win.buffer = CreateCompatibleBitmap(hdc, width, height); 8005 8006 if(oldBuffer) 8007 DeleteObject(oldBuffer); 8008 8009 win.bmpWidth = width; 8010 win.bmpHeight = height; 8011 } 8012 8013 // just always erase it upon resizing so minigui can draw over with a clean slate 8014 auto oldBmp = SelectObject(hdcBmp, win.buffer); 8015 8016 auto brush = GetSysColorBrush(COLOR_3DFACE); 8017 RECT r; 8018 r.left = 0; 8019 r.top = 0; 8020 r.right = width; 8021 r.bottom = height; 8022 FillRect(hdcBmp, &r, brush); 8023 8024 SelectObject(hdcBmp, oldBmp); 8025 DeleteDC(hdcBmp); 8026 ReleaseDC(hwnd, hdc); 8027 break; 8028 case WM_PAINT: 8029 if(win.buffer is null) 8030 goto default; 8031 8032 BITMAP bm; 8033 PAINTSTRUCT ps; 8034 8035 HDC hdc = BeginPaint(hwnd, &ps); 8036 8037 HDC hdcMem = CreateCompatibleDC(hdc); 8038 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 8039 8040 GetObject(win.buffer, bm.sizeof, &bm); 8041 8042 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 8043 8044 SelectObject(hdcMem, hbmOld); 8045 DeleteDC(hdcMem); 8046 EndPaint(hwnd, &ps); 8047 break; 8048 default: 8049 return DefWindowProc(hwnd, message, wparam, lparam); 8050 } 8051 8052 return 0; 8053 } 8054 8055 private wstring Win32Class(wstring name)() { 8056 static bool classRegistered; 8057 if(!classRegistered) { 8058 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 8059 WNDCLASSEX wc; 8060 wc.cbSize = wc.sizeof; 8061 wc.hInstance = hInstance; 8062 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 8063 wc.lpfnWndProc = &DoubleBufferWndProc; 8064 wc.lpszClassName = name.ptr; 8065 if(!RegisterClassExW(&wc)) 8066 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 8067 classRegistered = true; 8068 } 8069 8070 return name; 8071 } 8072 8073 /+ 8074 version(win32_widgets) 8075 extern(Windows) 8076 private 8077 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 8078 switch(iMessage) { 8079 case WM_PAINT: 8080 if(auto te = hWnd in Widget.nativeMapping) { 8081 try { 8082 //te.redraw(); 8083 writeln(te, " drawing"); 8084 } catch(Exception) {} 8085 } 8086 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8087 default: 8088 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8089 } 8090 } 8091 +/ 8092 8093 8094 /++ 8095 A widget specifically designed to hold other widgets. 8096 8097 History: 8098 Added July 1, 2021 8099 +/ 8100 class ContainerWidget : Widget { 8101 this(Widget parent) { 8102 super(parent); 8103 this.tabStop = false; 8104 8105 version(win32_widgets) { 8106 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 8107 } 8108 } 8109 } 8110 8111 /++ 8112 A widget that takes your widget, puts scroll bars around it, and sends 8113 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 8114 no effort to automatically scroll or clip its child widgets - it just sends 8115 the messages. 8116 8117 8118 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 8119 The scroll coordinates are all given in a unit you interpret as you wish. One 8120 of these units is moved on each press of the arrow buttons and represents the 8121 smallest amount the user can scroll. The intention is for this to be one line, 8122 one item in a list, one row in a table, etc. Whatever makes sense for your widget 8123 in each direction that the user might be interested in. 8124 8125 You can set a "page size" with the [step] property. (Yes, I regret the name...) 8126 This is the amount it jumps when the user pressed page up and page down, or clicks 8127 in the exposed part of the scroll bar. 8128 8129 You should add child content to the ScrollMessageWidget. However, it is important to 8130 note that the coordinates are always independent of the scroll position! It is YOUR 8131 responsibility to do any necessary transforms, clipping, etc., while drawing the 8132 content and interpreting mouse events if they are supposed to change with the scroll. 8133 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 8134 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 8135 you more control (which can be considerably more efficient and adapted to your actual data) 8136 at the expense of you also needing to be aware of its reality. 8137 8138 Please note that it does NOT react to mouse wheel events or various keyboard events as of 8139 version 10.3. Maybe this will change in the future.... but for now you must call 8140 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 8141 +/ 8142 class ScrollMessageWidget : Widget { 8143 this(Widget parent) { 8144 super(parent); 8145 8146 container = new Widget(this); 8147 hsb = new HorizontalScrollbar(this); 8148 vsb = new VerticalScrollbar(this); 8149 8150 hsb.addEventListener("scrolltonextline", { 8151 hsb.setPosition(hsb.position + movementPerButtonClickH_); 8152 notify(); 8153 }); 8154 hsb.addEventListener("scrolltopreviousline", { 8155 hsb.setPosition(hsb.position - movementPerButtonClickH_); 8156 notify(); 8157 }); 8158 vsb.addEventListener("scrolltonextline", { 8159 vsb.setPosition(vsb.position + movementPerButtonClickV_); 8160 notify(); 8161 }); 8162 vsb.addEventListener("scrolltopreviousline", { 8163 vsb.setPosition(vsb.position - movementPerButtonClickV_); 8164 notify(); 8165 }); 8166 hsb.addEventListener("scrolltonextpage", { 8167 hsb.setPosition(hsb.position + hsb.step_); 8168 notify(); 8169 }); 8170 hsb.addEventListener("scrolltopreviouspage", { 8171 hsb.setPosition(hsb.position - hsb.step_); 8172 notify(); 8173 }); 8174 vsb.addEventListener("scrolltonextpage", { 8175 vsb.setPosition(vsb.position + vsb.step_); 8176 notify(); 8177 }); 8178 vsb.addEventListener("scrolltopreviouspage", { 8179 vsb.setPosition(vsb.position - vsb.step_); 8180 notify(); 8181 }); 8182 hsb.addEventListener("scrolltoposition", (Event event) { 8183 hsb.setPosition(event.intValue); 8184 notify(); 8185 }); 8186 vsb.addEventListener("scrolltoposition", (Event event) { 8187 vsb.setPosition(event.intValue); 8188 notify(); 8189 }); 8190 8191 8192 tabStop = false; 8193 container.tabStop = false; 8194 magic = true; 8195 } 8196 8197 private int movementPerButtonClickH_ = 1; 8198 private int movementPerButtonClickV_ = 1; 8199 public void movementPerButtonClick(int h, int v) { 8200 movementPerButtonClickH_ = h; 8201 movementPerButtonClickV_ = v; 8202 } 8203 8204 /++ 8205 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 8206 8207 8208 The defaults for [addDefaultWheelListeners] are: 8209 8210 $(LIST 8211 * Mouse wheel scrolls vertically 8212 * Alt key + mouse wheel scrolls horiontally 8213 * Shift + mouse wheel scrolls faster. 8214 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 8215 ) 8216 8217 The defaults for [addDefaultKeyboardListeners] are: 8218 8219 $(LIST 8220 * Arrow keys scroll by the given amounts 8221 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 8222 * Page up and down scroll by the vertical viewable area 8223 * Home and end scroll to the start and end of the verticle viewable area. 8224 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 8225 ) 8226 8227 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 8228 8229 Params: 8230 horizontalArrowScrollAmount = 8231 verticalArrowScrollAmount = 8232 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 8233 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 8234 shiftMultiplier = multiplies the scroll amount by this when shift is held 8235 +/ 8236 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 8237 defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount; 8238 defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount; 8239 defaultKeyboardListener_shiftMultiplier = shiftMultiplier; 8240 8241 container.addEventListener(&defaultKeyboardListener); 8242 } 8243 8244 /// ditto 8245 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 8246 auto _this = this; 8247 container.addEventListener((scope ClickEvent ce) { 8248 8249 //if(ce.target && ce.target.tabStop) 8250 //ce.target.focus(); 8251 8252 // ctrl is reserved for the application 8253 if(ce.ctrlKey) 8254 return; 8255 8256 if(horizontalWheelScrollAmount == 0 && ce.altKey) 8257 return; 8258 8259 if(shiftMultiplier == 0 && ce.shiftKey) 8260 return; 8261 8262 if(ce.button == MouseButton.wheelDown) { 8263 if(ce.altKey) 8264 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8265 else 8266 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8267 } else if(ce.button == MouseButton.wheelUp) { 8268 if(ce.altKey) 8269 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8270 else 8271 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8272 } 8273 }); 8274 } 8275 8276 int defaultKeyboardListener_verticalArrowScrollAmount = 1; 8277 int defaultKeyboardListener_horizontalArrowScrollAmount = 1; 8278 int defaultKeyboardListener_shiftMultiplier = 3; 8279 8280 void defaultKeyboardListener(scope KeyDownEvent ke) { 8281 switch(ke.key) { 8282 case Key.Left: 8283 this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8284 break; 8285 case Key.Right: 8286 this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8287 break; 8288 case Key.Up: 8289 this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8290 break; 8291 case Key.Down: 8292 this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8293 break; 8294 case Key.PageUp: 8295 if(ke.altKey) 8296 this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8297 else 8298 this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8299 break; 8300 case Key.PageDown: 8301 if(ke.altKey) 8302 this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8303 else 8304 this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8305 break; 8306 case Key.Home: 8307 if(ke.altKey) 8308 this.scrollLeft(short.max * 16); 8309 else 8310 this.scrollUp(short.max * 16); 8311 break; 8312 case Key.End: 8313 if(ke.altKey) 8314 this.scrollRight(short.max * 16); 8315 else 8316 this.scrollDown(short.max * 16); 8317 break; 8318 8319 default: 8320 // ignore, not for us. 8321 } 8322 } 8323 8324 /++ 8325 Scrolls the given amount. 8326 8327 History: 8328 The scroll up and down functions was here in the initial release of the class, but the `amount` parameter and left/right functions were added on September 28, 2021. 8329 +/ 8330 void scrollUp(int amount = 1) { 8331 vsb.setPosition(vsb.position.NonOverflowingInt - amount); 8332 notify(); 8333 } 8334 /// ditto 8335 void scrollDown(int amount = 1) { 8336 vsb.setPosition(vsb.position.NonOverflowingInt + amount); 8337 notify(); 8338 } 8339 /// ditto 8340 void scrollLeft(int amount = 1) { 8341 hsb.setPosition(hsb.position.NonOverflowingInt - amount); 8342 notify(); 8343 } 8344 /// ditto 8345 void scrollRight(int amount = 1) { 8346 hsb.setPosition(hsb.position.NonOverflowingInt + amount); 8347 notify(); 8348 } 8349 8350 /// 8351 VerticalScrollbar verticalScrollBar() { return vsb; } 8352 /// 8353 HorizontalScrollbar horizontalScrollBar() { return hsb; } 8354 8355 void notify() { 8356 static bool insideNotify; 8357 8358 if(insideNotify) 8359 return; // avoid the recursive call, even if it isn't strictly correct 8360 8361 insideNotify = true; 8362 scope(exit) insideNotify = false; 8363 8364 this.emit!ScrollEvent(); 8365 } 8366 8367 mixin Emits!ScrollEvent; 8368 8369 /// 8370 Point position() { 8371 return Point(hsb.position, vsb.position); 8372 } 8373 8374 /// 8375 void setPosition(int x, int y) { 8376 hsb.setPosition(x); 8377 vsb.setPosition(y); 8378 } 8379 8380 /// 8381 void setPageSize(int unitsX, int unitsY) { 8382 hsb.setStep(unitsX); 8383 vsb.setStep(unitsY); 8384 } 8385 8386 /// Always call this BEFORE setViewableArea 8387 void setTotalArea(int width, int height) { 8388 hsb.setMax(width); 8389 vsb.setMax(height); 8390 } 8391 8392 /++ 8393 Always set the viewable area AFTER setitng the total area if you are going to change both. 8394 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 8395 If you need to do that, use [queueRecomputeChildLayout]. 8396 +/ 8397 void setViewableArea(int width, int height) { 8398 8399 // actually there IS A need to dothis cuz the max might have changed since then 8400 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 8401 //return; // no need to do what is already done 8402 hsb.setViewableArea(width); 8403 vsb.setViewableArea(height); 8404 8405 bool needsNotify = false; 8406 8407 // FIXME: if at any point the rhs is outside the scrollbar, we need 8408 // to reset to 0. but it should remember the old position in case the 8409 // window resizes again, so it can kinda return ot where it was. 8410 // 8411 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 8412 if(width >= hsb.max) { 8413 // there's plenty of room to display it all so we need to reset to zero 8414 // FIXME: adjust so it matches the note above 8415 hsb.setPosition(0); 8416 needsNotify = true; 8417 } 8418 if(height >= vsb.max) { 8419 // there's plenty of room to display it all so we need to reset to zero 8420 // FIXME: adjust so it matches the note above 8421 vsb.setPosition(0); 8422 needsNotify = true; 8423 } 8424 if(needsNotify) 8425 notify(); 8426 } 8427 8428 private bool magic; 8429 override void addChild(Widget w, int position = int.max) { 8430 if(magic) 8431 container.addChild(w, position); 8432 else 8433 super.addChild(w, position); 8434 } 8435 8436 override void recomputeChildLayout() { 8437 if(hsb is null || vsb is null || container is null) return; 8438 8439 registerMovement(); 8440 8441 enum BUTTON_SIZE = 16; 8442 8443 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 8444 hsb.x = 0; 8445 hsb.y = this.height - hsb.height; 8446 8447 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 8448 vsb.x = this.width - vsb.width; 8449 vsb.y = 0; 8450 8451 auto vsb_width = vsb.showing ? vsb.width : 0; 8452 auto hsb_height = hsb.showing ? hsb.height : 0; 8453 8454 hsb.width = this.width - vsb_width; 8455 vsb.height = this.height - hsb_height; 8456 8457 hsb.recomputeChildLayout(); 8458 vsb.recomputeChildLayout(); 8459 8460 if(this.header is null) { 8461 container.x = 0; 8462 container.y = 0; 8463 container.width = this.width - vsb_width; 8464 container.height = this.height - hsb_height; 8465 container.recomputeChildLayout(); 8466 } else { 8467 header.x = 0; 8468 header.y = 0; 8469 header.width = this.width - vsb_width; 8470 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 8471 header.recomputeChildLayout(); 8472 8473 container.x = 0; 8474 container.y = scaleWithDpi(BUTTON_SIZE); 8475 container.width = this.width - vsb_width; 8476 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 8477 container.recomputeChildLayout(); 8478 } 8479 } 8480 8481 private HorizontalScrollbar hsb; 8482 private VerticalScrollbar vsb; 8483 Widget container; 8484 private Widget header; 8485 8486 /++ 8487 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 8488 8489 History: 8490 Added September 27, 2021 (dub v10.3) 8491 +/ 8492 Widget getHeader() { 8493 if(this.header is null) { 8494 magic = false; 8495 scope(exit) magic = true; 8496 this.header = new Widget(this); 8497 queueRecomputeChildLayout(); 8498 } 8499 return this.header; 8500 } 8501 8502 /++ 8503 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 8504 8505 History: 8506 Added January 3, 2023 (dub v11.0) 8507 +/ 8508 void scrollIntoView(Rectangle rect) { 8509 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 8510 8511 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 8512 8513 // the lower right is exclusive normally 8514 auto test = rect.lowerRight; 8515 if(test.x > 0) test.x--; 8516 if(test.y > 0) test.y--; 8517 8518 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 8519 // try to scroll only one dimension at a time if we can 8520 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 8521 setPosition(rect.upperLeft.x, position.y); 8522 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 8523 setPosition(position.x, rect.upperLeft.y); 8524 } 8525 8526 } 8527 8528 override int minHeight() { 8529 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 8530 if(header !is null) 8531 min += header.minHeight; 8532 if(horizontalScrollBar.showing) 8533 min += horizontalScrollBar.minHeight; 8534 return min; 8535 } 8536 8537 override int maxHeight() { 8538 int max = container ? container.maxHeight : int.max; 8539 if(max == int.max) 8540 return max; 8541 if(horizontalScrollBar.showing) 8542 max += horizontalScrollBar.minHeight; 8543 return max; 8544 } 8545 8546 static class Style : Widget.Style { 8547 override WidgetBackground background() { 8548 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8549 } 8550 } 8551 mixin OverrideStyle!Style; 8552 } 8553 8554 /++ 8555 $(IMG //arsdnet.net/minigui-screenshots/windows/ScrollMessageWidget.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 8556 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 8557 +/ 8558 version(minigui_screenshots) 8559 @Screenshot("ScrollMessageWidget") 8560 unittest { 8561 auto window = new Window("ScrollMessageWidget"); 8562 8563 auto smw = new ScrollMessageWidget(window); 8564 smw.addDefaultKeyboardListeners(); 8565 smw.addDefaultWheelListeners(); 8566 8567 window.loop(); 8568 } 8569 8570 /++ 8571 Bypasses automatic layout for its children, using manual positioning and sizing only. 8572 While you need to manually position them, you must ensure they are inside the StaticLayout's 8573 bounding box to avoid undefined behavior. 8574 8575 You should almost never use this. 8576 +/ 8577 class StaticLayout : Layout { 8578 /// 8579 this(Widget parent) { super(parent); } 8580 override void recomputeChildLayout() { 8581 registerMovement(); 8582 foreach(child; children) 8583 child.recomputeChildLayout(); 8584 } 8585 } 8586 8587 /++ 8588 Bypasses automatic positioning when being laid out. It is your responsibility to make 8589 room for this widget in the parent layout. 8590 8591 Its children are laid out normally, unless there is exactly one, in which case it takes 8592 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 8593 can do that with `padding`). 8594 +/ 8595 class StaticPosition : Layout { 8596 /// 8597 this(Widget parent) { super(parent); } 8598 8599 override void recomputeChildLayout() { 8600 registerMovement(); 8601 if(this.children.length == 1) { 8602 auto child = children[0]; 8603 child.x = 0; 8604 child.y = 0; 8605 child.width = this.width; 8606 child.height = this.height; 8607 child.recomputeChildLayout(); 8608 } else 8609 foreach(child; children) 8610 child.recomputeChildLayout(); 8611 } 8612 8613 alias width = typeof(super).width; 8614 alias height = typeof(super).height; 8615 8616 @property int width(int w) @nogc pure @safe nothrow { 8617 return this._width = w; 8618 } 8619 8620 @property int height(int w) @nogc pure @safe nothrow { 8621 return this._height = w; 8622 } 8623 8624 } 8625 8626 /++ 8627 FixedPosition is like [StaticPosition], but its coordinates 8628 are always relative to the viewport, meaning they do not scroll with 8629 the parent content. 8630 +/ 8631 class FixedPosition : StaticPosition { 8632 /// 8633 this(Widget parent) { super(parent); } 8634 } 8635 8636 version(win32_widgets) 8637 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 8638 if(true) { 8639 // cmd == 0 = menu, cmd == 1 = accelerator 8640 if(auto item = idm in Action.mapping) { 8641 foreach(handler; (*item).triggered) 8642 handler(); 8643 /* 8644 auto event = new Event("triggered", *item); 8645 event.button = idm; 8646 event.dispatch(); 8647 */ 8648 return 0; 8649 } 8650 } 8651 if(handle) 8652 if(auto widgetp = handle in Widget.nativeMapping) { 8653 (*widgetp).handleWmCommand(cmd, idm); 8654 return 0; 8655 } 8656 return 1; 8657 } 8658 8659 8660 /// 8661 class Window : Widget { 8662 Widget[] mouseCapturedBy; 8663 void captureMouse(Widget byWhom) { 8664 assert(byWhom !is null); 8665 if(mouseCapturedBy.length > 0) { 8666 auto cc = mouseCapturedBy[$-1]; 8667 if(cc is byWhom) 8668 return; // or should it throw? 8669 auto par = byWhom; 8670 while(par) { 8671 if(cc is par) 8672 goto allowed; 8673 par = par.parent; 8674 } 8675 8676 throw new Exception("mouse is already captured by other widget"); 8677 } 8678 allowed: 8679 mouseCapturedBy ~= byWhom; 8680 if(mouseCapturedBy.length == 1) 8681 win.grabInput(false, true, false); 8682 //void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) { 8683 } 8684 void releaseMouseCapture() { 8685 if(mouseCapturedBy.length == 0) 8686 return; // or should it throw? 8687 mouseCapturedBy = mouseCapturedBy[0 .. $-1]; 8688 mouseCapturedBy.assumeSafeAppend(); 8689 if(mouseCapturedBy.length == 0) 8690 win.releaseInputGrab(); 8691 } 8692 8693 8694 /++ 8695 8696 +/ 8697 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8698 return .messageBox(this, title, message, style, icon); 8699 } 8700 8701 /// ditto 8702 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8703 return messageBox(null, message, style, icon); 8704 } 8705 8706 8707 /++ 8708 Sets the window icon which is often seen in title bars and taskbars. 8709 8710 A future plan is to offer an overload that takes an array too for multiple sizes, but right now you should probably set 16x16 or 32x32 images here. 8711 8712 History: 8713 Added April 5, 2022 (dub v10.8) 8714 +/ 8715 @property void icon(MemoryImage icon) { 8716 if(win && icon) 8717 win.icon = icon; 8718 } 8719 8720 // forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples 8721 // this does NOT change the icon on the window! That's what the other overload is for 8722 static @property .icon icon(GenericIcons i) { 8723 return .icon(i); 8724 } 8725 8726 /// 8727 @scriptable 8728 @property bool focused() { 8729 return win.focused; 8730 } 8731 8732 static class Style : Widget.Style { 8733 override WidgetBackground background() { 8734 version(custom_widgets) 8735 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8736 else version(win32_widgets) 8737 return WidgetBackground(Color.transparent); 8738 else static assert(0); 8739 } 8740 } 8741 mixin OverrideStyle!Style; 8742 8743 /++ 8744 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. 8745 +/ 8746 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8747 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8748 } 8749 8750 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8751 OperatingSystemFont font; 8752 if(auto vt = WidgetPainter.visualTheme) { 8753 font = vt.defaultFontCached(96); // FIXME 8754 } 8755 8756 if(font is null) { 8757 static int defaultHeightCache; 8758 if(defaultHeightCache == 0) { 8759 font = new OperatingSystemFont; 8760 font.loadDefault; 8761 defaultHeightCache = font.height();// * 5 / 4; 8762 } 8763 return defaultHeightCache; 8764 } 8765 8766 return font.height();// * 5 / 4; 8767 } 8768 8769 Widget focusedWidget; 8770 8771 private SimpleWindow win_; 8772 8773 @property { 8774 /++ 8775 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8776 8777 History: 8778 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8779 +/ 8780 public SimpleWindow win() { 8781 return win_; 8782 } 8783 /// 8784 protected void win(SimpleWindow w) { 8785 win_ = w; 8786 } 8787 } 8788 8789 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8790 this(Widget p) { 8791 tabStop = false; 8792 super(p); 8793 } 8794 8795 private void actualRedraw() { 8796 if(recomputeChildLayoutRequired) 8797 recomputeChildLayoutEntry(); 8798 if(!showing) return; 8799 8800 assert(parentWindow !is null); 8801 8802 auto w = drawableWindow; 8803 if(w is null) 8804 w = parentWindow.win; 8805 8806 if(w.closed()) 8807 return; 8808 8809 auto ugh = this.parent; 8810 int lox, loy; 8811 while(ugh) { 8812 lox += ugh.x; 8813 loy += ugh.y; 8814 ugh = ugh.parent; 8815 } 8816 auto painter = w.draw(true); 8817 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8818 } 8819 8820 8821 private bool skipNextChar = false; 8822 8823 /++ 8824 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8825 8826 This constructor is intended primarily for internal use and may be changed to `protected` later. 8827 +/ 8828 this(SimpleWindow win) { 8829 8830 static if(UsingSimpledisplayX11) { 8831 win.discardAdditionalConnectionState = &discardXConnectionState; 8832 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8833 } 8834 8835 tabStop = false; 8836 super(null); 8837 this.win = win; 8838 8839 win.addEventListener((Widget.RedrawEvent) { 8840 if(win.eventQueued!RecomputeEvent) { 8841 // writeln("skipping"); 8842 return; // let the recompute event do the actual redraw 8843 } 8844 this.actualRedraw(); 8845 }); 8846 8847 win.addEventListener((Widget.RecomputeEvent) { 8848 recomputeChildLayoutEntry(); 8849 if(win.eventQueued!RedrawEvent) 8850 return; // let the queued one do it 8851 else { 8852 // writeln("drawing"); 8853 this.actualRedraw(); // if not queued, it needs to be done now anyway 8854 } 8855 }); 8856 8857 this.width = win.width; 8858 this.height = win.height; 8859 this.parentWindow = this; 8860 8861 win.closeQuery = () { 8862 if(this.emit!ClosingEvent()) 8863 win.close(); 8864 }; 8865 win.onClosing = () { 8866 this.emit!ClosedEvent(); 8867 }; 8868 8869 win.windowResized = (int w, int h) { 8870 this.width = w; 8871 this.height = h; 8872 queueRecomputeChildLayout(); 8873 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8874 //version(win32_widgets) 8875 //InvalidateRect(hwnd, null, true); 8876 redraw(); 8877 }; 8878 8879 win.onFocusChange = (bool getting) { 8880 // sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString); 8881 if(this.focusedWidget) { 8882 if(getting) { 8883 this.focusedWidget.emit!FocusEvent(); 8884 this.focusedWidget.emit!FocusInEvent(); 8885 } else { 8886 this.focusedWidget.emit!BlurEvent(); 8887 this.focusedWidget.emit!FocusOutEvent(); 8888 } 8889 } 8890 8891 if(getting) { 8892 this.emit!FocusEvent(); 8893 this.emit!FocusInEvent(); 8894 } else { 8895 this.emit!BlurEvent(); 8896 this.emit!FocusOutEvent(); 8897 } 8898 }; 8899 8900 win.onDpiChanged = { 8901 this.queueRecomputeChildLayout(); 8902 auto event = new DpiChangedEvent(this); 8903 event.sendDirectly(); 8904 8905 privateDpiChanged(); 8906 }; 8907 8908 win.setEventHandlers( 8909 (MouseEvent e) { 8910 dispatchMouseEvent(e); 8911 }, 8912 (KeyEvent e) { 8913 //writefln("%x %s", cast(uint) e.key, e.key); 8914 dispatchKeyEvent(e); 8915 }, 8916 (dchar e) { 8917 if(e == 13) e = 10; // hack? 8918 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8919 dispatchCharEvent(e); 8920 }, 8921 ); 8922 8923 addEventListener("char", (Widget, Event ev) { 8924 if(skipNextChar) { 8925 ev.preventDefault(); 8926 skipNextChar = false; 8927 } 8928 }); 8929 8930 version(win32_widgets) 8931 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8932 if(hwnd !is this.win.impl.hwnd) 8933 return 1; // we don't care... pass it on 8934 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8935 if(mustReturn) 8936 return ret; 8937 return 1; // pass it on 8938 }; 8939 8940 if(Window.newWindowCreated) 8941 Window.newWindowCreated(this); 8942 } 8943 8944 version(custom_widgets) 8945 override void defaultEventHandler_click(ClickEvent event) { 8946 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 8947 if(event.target && event.target.tabStop) 8948 event.target.focus(); 8949 } 8950 } 8951 8952 private static void delegate(Window) newWindowCreated; 8953 8954 version(win32_widgets) 8955 override void paint(WidgetPainter painter) { 8956 /* 8957 RECT rect; 8958 rect.right = this.width; 8959 rect.bottom = this.height; 8960 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8961 */ 8962 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8963 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8964 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8965 // since the pen is null, to fill the whole space, we need the +1 on both. 8966 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8967 SelectObject(painter.impl.hdc, p); 8968 SelectObject(painter.impl.hdc, b); 8969 } 8970 version(custom_widgets) 8971 override void paint(WidgetPainter painter) { 8972 auto cs = getComputedStyle(); 8973 painter.fillColor = cs.windowBackgroundColor; 8974 painter.outlineColor = cs.windowBackgroundColor; 8975 painter.drawRectangle(Point(0, 0), this.width, this.height); 8976 } 8977 8978 8979 override void defaultEventHandler_keydown(KeyDownEvent event) { 8980 Widget _this = event.target; 8981 8982 if(event.key == Key.Tab) { 8983 /* Window tab ordering is a recursive thingy with each group */ 8984 8985 // FIXME inefficient 8986 Widget[] helper(Widget p) { 8987 if(p.hidden) 8988 return null; 8989 Widget[] childOrdering; 8990 8991 auto children = p.children.dup; 8992 8993 while(true) { 8994 // UIs should be generally small, so gonna brute force it a little 8995 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8996 8997 Widget smallestTab; 8998 foreach(ref c; children) { 8999 if(c is null) continue; 9000 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 9001 smallestTab = c; 9002 c = null; 9003 } 9004 } 9005 if(smallestTab !is null) { 9006 if(smallestTab.tabStop && !smallestTab.hidden) 9007 childOrdering ~= smallestTab; 9008 if(!smallestTab.hidden) 9009 childOrdering ~= helper(smallestTab); 9010 } else 9011 break; 9012 9013 } 9014 9015 return childOrdering; 9016 } 9017 9018 Widget[] tabOrdering = helper(this); 9019 9020 Widget recipient; 9021 9022 if(tabOrdering.length) { 9023 bool seenThis = false; 9024 Widget previous; 9025 foreach(idx, child; tabOrdering) { 9026 if(child is focusedWidget) { 9027 9028 if(event.shiftKey) { 9029 if(idx == 0) 9030 recipient = tabOrdering[$-1]; 9031 else 9032 recipient = tabOrdering[idx - 1]; 9033 break; 9034 } 9035 9036 seenThis = true; 9037 if(idx + 1 == tabOrdering.length) { 9038 // we're at the end, either move to the next group 9039 // or start back over 9040 recipient = tabOrdering[0]; 9041 } 9042 continue; 9043 } 9044 if(seenThis) { 9045 recipient = child; 9046 break; 9047 } 9048 previous = child; 9049 } 9050 } 9051 9052 if(recipient !is null) { 9053 // writeln(typeid(recipient)); 9054 recipient.focus(); 9055 9056 skipNextChar = true; 9057 } 9058 } 9059 9060 debug if(event.key == Key.F12) { 9061 if(devTools) { 9062 devTools.close(); 9063 devTools = null; 9064 } else { 9065 devTools = new DevToolWindow(this); 9066 devTools.show(); 9067 } 9068 } 9069 } 9070 9071 debug DevToolWindow devTools; 9072 9073 9074 /++ 9075 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 9076 9077 History: 9078 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 9079 9080 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 9081 +/ 9082 this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9083 if(title is null) { 9084 import core.runtime; 9085 if(Runtime.args.length) 9086 title = Runtime.args[0]; 9087 } 9088 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent); 9089 9090 static if(UsingSimpledisplayX11) 9091 if(windowFlags & WindowFlags.managesChildWindowFocus) { 9092 ///+ 9093 // for input proxy 9094 auto display = XDisplayConnection.get; 9095 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 9096 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 9097 XMapWindow(display, inputProxy); 9098 // writefln("input proxy: 0x%0x", inputProxy); 9099 this.inputProxy = new SimpleWindow(inputProxy); 9100 9101 /+ 9102 this.inputProxy.onFocusChange = (bool getting) { 9103 sdpyPrintDebugString("input proxy focus change ", getting); 9104 }; 9105 +/ 9106 9107 XEvent lastEvent; 9108 this.inputProxy.handleNativeEvent = (XEvent ev) { 9109 lastEvent = ev; 9110 return 1; 9111 }; 9112 this.inputProxy.setEventHandlers( 9113 (MouseEvent e) { 9114 dispatchMouseEvent(e); 9115 }, 9116 (KeyEvent e) { 9117 //writefln("%x %s", cast(uint) e.key, e.key); 9118 if(dispatchKeyEvent(e)) { 9119 // FIXME: i should trap error 9120 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 9121 auto thing = nw.focusableWindow(); 9122 if(thing && thing.window) { 9123 lastEvent.xkey.window = thing.window; 9124 // writeln("sending event ", lastEvent.xkey); 9125 trapXErrors( { 9126 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 9127 }); 9128 } 9129 } 9130 } 9131 }, 9132 (dchar e) { 9133 if(e == 13) e = 10; // hack? 9134 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9135 dispatchCharEvent(e); 9136 }, 9137 ); 9138 9139 this.inputProxy.populateXic(); 9140 // done 9141 //+/ 9142 } 9143 9144 9145 9146 win.setRequestedInputFocus = &this.setRequestedInputFocus; 9147 9148 this(win); 9149 } 9150 9151 SimpleWindow inputProxy; 9152 9153 private SimpleWindow setRequestedInputFocus() { 9154 return inputProxy; 9155 } 9156 9157 /// ditto 9158 this(string title, int width = 500, int height = 500) { 9159 this(width, height, title); 9160 } 9161 9162 /// 9163 @property string title() { return parentWindow.win.title; } 9164 /// 9165 @property void title(string title) { parentWindow.win.title = title; } 9166 9167 /// 9168 @scriptable 9169 void close() { 9170 win.close(); 9171 // I synchronize here upon window closing to ensure all child windows 9172 // get updated too before the event loop. This avoids some random X errors. 9173 static if(UsingSimpledisplayX11) { 9174 runInGuiThread( { 9175 XSync(XDisplayConnection.get, false); 9176 }); 9177 } 9178 } 9179 9180 bool dispatchKeyEvent(KeyEvent ev) { 9181 auto wid = focusedWidget; 9182 if(wid is null) 9183 wid = this; 9184 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 9185 event.originalKeyEvent = ev; 9186 event.key = ev.key; 9187 event.state = ev.modifierState; 9188 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9189 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9190 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9191 event.dispatch(); 9192 9193 return !event.propagationStopped; 9194 } 9195 9196 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 9197 bool dispatchCharEvent(dchar ch) { 9198 if(focusedWidget) { 9199 auto event = new CharEvent(focusedWidget, ch); 9200 event.dispatch(); 9201 return !event.propagationStopped; 9202 } 9203 return true; 9204 } 9205 9206 Widget mouseLastOver; 9207 Widget mouseLastDownOn; 9208 bool lastWasDoubleClick; 9209 bool dispatchMouseEvent(MouseEvent ev) { 9210 auto eleR = widgetAtPoint(this, ev.x, ev.y); 9211 auto ele = eleR.widget; 9212 9213 auto captureEle = ele; 9214 9215 auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null; 9216 if(mouseCapturedBy !is null) { 9217 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 9218 captureEle = mouseCapturedBy; 9219 } 9220 9221 // a hack to get it relative to the widget. 9222 eleR.x = ev.x; 9223 eleR.y = ev.y; 9224 auto pain = captureEle; 9225 while(pain) { 9226 eleR.x -= pain.x; 9227 eleR.y -= pain.y; 9228 pain.addScrollPosition(eleR.x, eleR.y); 9229 pain = pain.parent; 9230 } 9231 9232 void populateMouseEventBase(MouseEventBase event) { 9233 event.button = ev.button; 9234 event.buttonLinear = ev.buttonLinear; 9235 event.state = ev.modifierState; 9236 event.clientX = eleR.x; 9237 event.clientY = eleR.y; 9238 9239 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9240 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9241 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9242 } 9243 9244 if(ev.type == MouseEventType.buttonPressed) { 9245 { 9246 auto event = new MouseDownEvent(captureEle); 9247 populateMouseEventBase(event); 9248 event.dispatch(); 9249 } 9250 9251 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 9252 auto event = new DoubleClickEvent(captureEle); 9253 populateMouseEventBase(event); 9254 event.dispatch(); 9255 lastWasDoubleClick = ev.doubleClick; 9256 } else { 9257 lastWasDoubleClick = false; 9258 } 9259 9260 mouseLastDownOn = ele; 9261 } else if(ev.type == MouseEventType.buttonReleased) { 9262 { 9263 auto event = new MouseUpEvent(captureEle); 9264 populateMouseEventBase(event); 9265 event.dispatch(); 9266 } 9267 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 9268 auto event = new ClickEvent(captureEle); 9269 populateMouseEventBase(event); 9270 event.dispatch(); 9271 } 9272 } else if(ev.type == MouseEventType.motion) { 9273 // motion 9274 { 9275 auto event = new MouseMoveEvent(captureEle); 9276 populateMouseEventBase(event); // fills in button which is meaningless but meh 9277 event.dispatch(); 9278 } 9279 9280 if(mouseLastOver !is ele) { 9281 if(ele !is null) { 9282 if(!isAParentOf(ele, mouseLastOver)) { 9283 ele.setDynamicState(DynamicState.hover, true); 9284 auto event = new MouseEnterEvent(ele); 9285 event.relatedTarget = mouseLastOver; 9286 event.sendDirectly(); 9287 9288 ele.useStyleProperties((scope Widget.Style s) { 9289 ele.parentWindow.win.cursor = s.cursor; 9290 }); 9291 } 9292 } 9293 9294 if(mouseLastOver !is null) { 9295 if(!isAParentOf(mouseLastOver, ele)) { 9296 mouseLastOver.setDynamicState(DynamicState.hover, false); 9297 auto event = new MouseLeaveEvent(mouseLastOver); 9298 event.relatedTarget = ele; 9299 event.sendDirectly(); 9300 } 9301 } 9302 9303 if(ele !is null) { 9304 auto event = new MouseOverEvent(ele); 9305 event.relatedTarget = mouseLastOver; 9306 event.dispatch(); 9307 } 9308 9309 if(mouseLastOver !is null) { 9310 auto event = new MouseOutEvent(mouseLastOver); 9311 event.relatedTarget = ele; 9312 event.dispatch(); 9313 } 9314 9315 mouseLastOver = ele; 9316 } 9317 } 9318 9319 return true; // FIXME: the event default prevented? 9320 } 9321 9322 /++ 9323 Shows the window and runs the application event loop. 9324 9325 Blocks until this window is closed. 9326 9327 Bugs: 9328 9329 $(PITFALL 9330 You should always have one event loop live for your application. 9331 If you make two windows in sequence, the second call to loop (or 9332 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 9333 might fail: 9334 9335 --- 9336 // don't do this! 9337 auto window = new Window(); 9338 window.loop(); 9339 9340 // or new Window or new MainWindow, all the same 9341 auto window2 = new SimpleWindow(); 9342 window2.eventLoop(0); // problematic! might crash 9343 --- 9344 9345 simpledisplay's current implementation assumes that final cleanup is 9346 done when the event loop refcount reaches zero. So after the first 9347 eventLoop returns, when there isn't already another one active, it assumes 9348 the program will exit soon and cleans up. 9349 9350 This is arguably a bug that it doesn't reinitialize, and I'll probably change 9351 it eventually, but in the mean time, there's an easy solution: 9352 9353 --- 9354 // do this 9355 EventLoop mainEventLoop = EventLoop.get; // just add this line 9356 9357 auto window = new Window(); 9358 window.loop(); 9359 9360 // or any other type of Window etc. 9361 auto window2 = new Window(); 9362 window2.loop(); // perfectly fine since mainEventLoop still alive 9363 --- 9364 9365 By adding a top-level reference to the event loop, it ensures the final cleanup 9366 is not performed until it goes out of scope too, letting the individual window loops 9367 work without trouble despite the bug. 9368 ) 9369 9370 History: 9371 The [BlockingMode] parameter was added on December 8, 2021. 9372 The default behavior is to block until the application quits 9373 (so all windows have been closed), unless another minigui or 9374 simpledisplay event loop is already running, in which case it 9375 will block until this window closes specifically. 9376 +/ 9377 @scriptable 9378 void loop(BlockingMode bm = BlockingMode.automatic) { 9379 if(win.closed) 9380 return; // otherwise show will throw 9381 show(); 9382 win.eventLoopWithBlockingMode(bm, 0); 9383 } 9384 9385 private bool firstShow = true; 9386 9387 @scriptable 9388 override void show() { 9389 bool rd = false; 9390 if(firstShow) { 9391 firstShow = false; 9392 queueRecomputeChildLayout(); 9393 // unless the programmer already called focus on something, pick something ourselves 9394 auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus? 9395 if(f) 9396 f.focus(); 9397 redraw(); 9398 } 9399 win.show(); 9400 super.show(); 9401 } 9402 @scriptable 9403 override void hide() { 9404 win.hide(); 9405 super.hide(); 9406 } 9407 9408 static Widget getFirstFocusable(Widget start) { 9409 if(start is null) 9410 return null; 9411 9412 foreach(widget; &start.focusableWidgets) { 9413 return widget; 9414 } 9415 9416 return null; 9417 } 9418 9419 static Widget getLastFocusable(Widget start) { 9420 if(start is null) 9421 return null; 9422 9423 Widget last; 9424 foreach(widget; &start.focusableWidgets) { 9425 last = widget; 9426 } 9427 9428 return last; 9429 } 9430 9431 9432 mixin Emits!ClosingEvent; 9433 mixin Emits!ClosedEvent; 9434 } 9435 9436 /++ 9437 History: 9438 Added January 12, 2022 9439 +/ 9440 class DpiChangedEvent : Event { 9441 enum EventString = "dpichanged"; 9442 9443 this(Widget target) { 9444 super(EventString, target); 9445 } 9446 } 9447 9448 debug private class DevToolWindow : Window { 9449 Window p; 9450 9451 TextEdit parentList; 9452 TextEdit logWindow; 9453 TextLabel clickX, clickY; 9454 9455 this(Window p) { 9456 this.p = p; 9457 super(400, 300, "Developer Toolbox"); 9458 9459 logWindow = new TextEdit(this); 9460 parentList = new TextEdit(this); 9461 9462 auto hl = new HorizontalLayout(this); 9463 clickX = new TextLabel("", TextAlignment.Right, hl); 9464 clickY = new TextLabel("", TextAlignment.Right, hl); 9465 9466 parentListeners ~= p.addEventListener("*", (Event ev) { 9467 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 9468 }); 9469 9470 parentListeners ~= p.addEventListener((ClickEvent ev) { 9471 auto s = ev.srcElement; 9472 9473 string list; 9474 9475 void addInfo(Widget s) { 9476 list ~= s.toString(); 9477 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 9478 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 9479 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 9480 list ~= "\n\theight: " ~ toInternal!string(s.height); 9481 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 9482 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 9483 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 9484 list ~= "\n\twidth: " ~ toInternal!string(s.width); 9485 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 9486 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 9487 } 9488 9489 addInfo(s); 9490 9491 s = s.parent; 9492 while(s) { 9493 list ~= "\n"; 9494 addInfo(s); 9495 s = s.parent; 9496 } 9497 parentList.content = list; 9498 9499 clickX.label = toInternal!string(ev.clientX); 9500 clickY.label = toInternal!string(ev.clientY); 9501 }); 9502 } 9503 9504 EventListener[] parentListeners; 9505 9506 override void close() { 9507 assert(p !is null); 9508 foreach(p; parentListeners) 9509 p.disconnect(); 9510 parentListeners = null; 9511 p.devTools = null; 9512 p = null; 9513 super.close(); 9514 } 9515 9516 override void defaultEventHandler_keydown(KeyDownEvent ev) { 9517 if(ev.key == Key.F12) { 9518 this.close(); 9519 if(p) 9520 p.devTools = null; 9521 } else { 9522 super.defaultEventHandler_keydown(ev); 9523 } 9524 } 9525 9526 void log(T...)(T t) { 9527 string str; 9528 import std.conv; 9529 foreach(i; t) 9530 str ~= to!string(i); 9531 str ~= "\n"; 9532 logWindow.addText(str); 9533 logWindow.scrollToBottom(); 9534 9535 //version(custom_widgets) 9536 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 9537 } 9538 } 9539 9540 /++ 9541 A dialog is a transient window that intends to get information from 9542 the user before being dismissed. 9543 +/ 9544 class Dialog : Window { 9545 /// 9546 this(Window parent, int width, int height, string title = null) { 9547 super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win); 9548 9549 // this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9550 } 9551 9552 /// 9553 this(Window parent, string title, int width, int height) { 9554 this(parent, width, height, title); 9555 } 9556 9557 deprecated("Pass an explicit parent window, even if it is `null`") 9558 this(int width, int height, string title = null) { 9559 this(null, width, height, title); 9560 } 9561 9562 /// 9563 void OK() { 9564 9565 } 9566 9567 /// 9568 void Cancel() { 9569 this.close(); 9570 } 9571 } 9572 9573 /++ 9574 A custom widget similar to the HTML5 <details> tag. 9575 +/ 9576 version(none) 9577 class DetailsView : Widget { 9578 9579 } 9580 9581 // FIXME: maybe i should expose the other list views Windows offers too 9582 9583 /++ 9584 A TableView is a widget made to display a table of data strings. 9585 9586 9587 Future_Directions: 9588 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 9589 9590 I will add a selection changed event at some point, as well as item clicked events. 9591 History: 9592 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 9593 See_Also: 9594 [ListWidget] which displays a list of strings without additional columns. 9595 +/ 9596 class TableView : Widget { 9597 /++ 9598 9599 +/ 9600 this(Widget parent) { 9601 super(parent); 9602 9603 version(win32_widgets) { 9604 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 9605 } else version(custom_widgets) { 9606 auto smw = new ScrollMessageWidget(this); 9607 smw.addDefaultKeyboardListeners(); 9608 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 9609 tvwi = new TableViewWidgetInner(this, smw); 9610 } 9611 } 9612 9613 // FIXME: auto-size columns on double click of header thing like in Windows 9614 // it need only make the currently displayed things fit well. 9615 9616 9617 private ColumnInfo[] columns; 9618 private int itemCount; 9619 9620 version(custom_widgets) private { 9621 TableViewWidgetInner tvwi; 9622 } 9623 9624 /// Passed to [setColumnInfo] 9625 static struct ColumnInfo { 9626 const(char)[] name; /// the name displayed in the header 9627 /++ 9628 The default width, in pixels. As a special case, you can set this to -1 9629 if you want the system to try to automatically size the width to fit visible 9630 content. If it can't, it will try to pick a sensible default size. 9631 9632 Any other negative value is not allowed and may lead to unpredictable results. 9633 9634 History: 9635 The -1 behavior was specified on December 3, 2021. It actually worked before 9636 anyway on Win32 but now it is a formal feature with partial Linux support. 9637 9638 Bugs: 9639 It doesn't actually attempt to calculate a best-fit width on Linux as of 9640 December 3, 2021. I do plan to fix this in the future, but Windows is the 9641 priority right now. At least it doesn't break things when you use it now. 9642 +/ 9643 int width; 9644 9645 /++ 9646 Alignment of the text in the cell. Applies to the header as well as all data in this 9647 column. 9648 9649 Bugs: 9650 On Windows, the first column ignores this member and is always left aligned. 9651 You can work around this by inserting a dummy first column with width = 0 9652 then putting your actual data in the second column, which does respect the 9653 alignment. 9654 9655 This is a quirk of the operating system's implementation going back a very 9656 long time and is unlikely to ever be fixed. 9657 +/ 9658 TextAlignment alignment; 9659 9660 /++ 9661 After all the pixel widths have been assigned, any left over 9662 space is divided up among all columns and distributed to according 9663 to the widthPercent field. 9664 9665 9666 For example, if you have two fields, both with width 50 and one with 9667 widthPercent of 25 and the other with widthPercent of 75, and the 9668 container is 200 pixels wide, first both get their width of 50. 9669 then the 100 remaining pixels are split up, so the one gets a total 9670 of 75 pixels and the other gets a total of 125. 9671 9672 This is automatically applied as the window is resized. 9673 9674 If there is not enough space - that is, when a horizontal scrollbar 9675 needs to appear - there are 0 pixels divided up, and thus everyone 9676 gets 0. This can cause a column to shrink out of proportion when 9677 passing the scroll threshold. 9678 9679 It is important to still set a fixed width (that is, to populate the 9680 `width` field) even if you use the percents because that will be the 9681 default minimum in the event of a scroll bar appearing. 9682 9683 The percents total in the column can never exceed 100 or be less than 0. 9684 Doing this will trigger an assert error. 9685 9686 Implementation note: 9687 9688 Please note that percentages are only recalculated 1) upon original 9689 construction and 2) upon resizing the control. If the user adjusts the 9690 width of a column, the percentage items will not be updated. 9691 9692 On the other hand, if the user adjusts the width of a percentage column 9693 then resizes the window, it is recalculated, meaning their hand adjustment 9694 is discarded. This specific behavior may change in the future as it is 9695 arguably a bug, but I'm not certain yet. 9696 9697 History: 9698 Added November 10, 2021 (dub v10.4) 9699 +/ 9700 int widthPercent; 9701 9702 9703 private int calculatedWidth; 9704 } 9705 /++ 9706 Sets the number of columns along with information about the headers. 9707 9708 Please note: on Windows, the first column ignores your alignment preference 9709 and is always left aligned. 9710 +/ 9711 void setColumnInfo(ColumnInfo[] columns...) { 9712 9713 foreach(ref c; columns) { 9714 c.name = c.name.idup; 9715 } 9716 this.columns = columns.dup; 9717 9718 updateCalculatedWidth(false); 9719 9720 version(custom_widgets) { 9721 tvwi.header.updateHeaders(); 9722 tvwi.updateScrolls(); 9723 } else version(win32_widgets) 9724 foreach(i, column; this.columns) { 9725 LVCOLUMN lvColumn; 9726 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 9727 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 9728 9729 auto bfr = WCharzBuffer(column.name); 9730 lvColumn.pszText = bfr.ptr; 9731 9732 if(column.alignment & TextAlignment.Center) 9733 lvColumn.fmt = LVCFMT_CENTER; 9734 else if(column.alignment & TextAlignment.Right) 9735 lvColumn.fmt = LVCFMT_RIGHT; 9736 else 9737 lvColumn.fmt = LVCFMT_LEFT; 9738 9739 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 9740 throw new WindowsApiException("Insert Column Fail", GetLastError()); 9741 } 9742 } 9743 9744 private int getActualSetSize(size_t i, bool askWindows) { 9745 version(win32_widgets) 9746 if(askWindows) 9747 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 9748 auto w = columns[i].width; 9749 if(w == -1) 9750 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 9751 return w; 9752 } 9753 9754 private void updateCalculatedWidth(bool informWindows) { 9755 int padding; 9756 version(win32_widgets) 9757 padding = 4; 9758 int remaining = this.width; 9759 foreach(i, column; columns) 9760 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9761 remaining -= padding; 9762 if(remaining < 0) 9763 remaining = 0; 9764 9765 int percentTotal; 9766 foreach(i, ref column; columns) { 9767 percentTotal += column.widthPercent; 9768 9769 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9770 9771 column.calculatedWidth = c; 9772 9773 version(win32_widgets) 9774 if(informWindows) 9775 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9776 } 9777 9778 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9779 assert(percentTotal <= 100, "The total percents in your column definitions exceeded 100. They must add up to no more than 100 (can be less though)."); 9780 9781 9782 } 9783 9784 override void registerMovement() { 9785 super.registerMovement(); 9786 9787 updateCalculatedWidth(true); 9788 } 9789 9790 /++ 9791 Tells the view how many items are in it. It uses this to set the scroll bar, but the items are not added per se; it calls [getData] as-needed. 9792 +/ 9793 void setItemCount(int count) { 9794 this.itemCount = count; 9795 version(custom_widgets) { 9796 tvwi.updateScrolls(); 9797 redraw(); 9798 } else version(win32_widgets) { 9799 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9800 } 9801 } 9802 9803 /++ 9804 Clears all items; 9805 +/ 9806 void clear() { 9807 this.itemCount = 0; 9808 this.columns = null; 9809 version(custom_widgets) { 9810 tvwi.header.updateHeaders(); 9811 tvwi.updateScrolls(); 9812 redraw(); 9813 } else version(win32_widgets) { 9814 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9815 } 9816 } 9817 9818 /+ 9819 version(win32_widgets) 9820 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9821 auto itemId = dis.itemID; 9822 auto hdc = dis.hDC; 9823 auto rect = dis.rcItem; 9824 switch(dis.itemAction) { 9825 case ODA_DRAWENTIRE: 9826 9827 // FIXME: do other items 9828 // FIXME: do the focus rectangle i guess 9829 // FIXME: alignment 9830 // FIXME: column width 9831 // FIXME: padding left 9832 // FIXME: check dpi scaling 9833 // FIXME: don't owner draw unless it is necessary. 9834 9835 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9836 RECT itemRect; 9837 itemRect.top = 1; // subitem idx, 1-based 9838 itemRect.left = LVIR_BOUNDS; 9839 9840 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9841 itemRect.left += padding; 9842 9843 getData(itemId, 0, (in char[] data) { 9844 auto wdata = WCharzBuffer(data); 9845 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9846 9847 }); 9848 goto case; 9849 case ODA_FOCUS: 9850 if(dis.itemState & ODS_FOCUS) 9851 DrawFocusRect(hdc, &rect); 9852 break; 9853 case ODA_SELECT: 9854 // itemState & ODS_SELECTED 9855 break; 9856 default: 9857 } 9858 return 1; 9859 } 9860 +/ 9861 9862 version(win32_widgets) { 9863 CellStyle last; 9864 COLORREF defaultColor; 9865 COLORREF defaultBackground; 9866 } 9867 9868 version(win32_widgets) 9869 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9870 switch(code) { 9871 case NM_CUSTOMDRAW: 9872 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9873 switch(s.nmcd.dwDrawStage) { 9874 case CDDS_PREPAINT: 9875 if(getCellStyle is null) 9876 return 0; 9877 9878 mustReturn = true; 9879 return CDRF_NOTIFYITEMDRAW; 9880 case CDDS_ITEMPREPAINT: 9881 mustReturn = true; 9882 return CDRF_NOTIFYSUBITEMDRAW; 9883 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9884 mustReturn = true; 9885 9886 if(getCellStyle is null) // this SHOULD never happen... 9887 return 0; 9888 9889 if(s.iSubItem == 0) { 9890 // Windows resets it per row so we'll use item 0 as a chance 9891 // to capture these for later 9892 defaultColor = s.clrText; 9893 defaultBackground = s.clrTextBk; 9894 } 9895 9896 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9897 // if no special style and no reset needed... 9898 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9899 return 0; // allow default processing to continue 9900 9901 last = style; 9902 9903 // might still need to reset or use the preference. 9904 9905 if(style.flags & CellStyle.Flags.textColorSet) 9906 s.clrText = style.textColor.asWindowsColorRef; 9907 else 9908 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9909 if(style.flags & CellStyle.Flags.backgroundColorSet) 9910 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9911 else 9912 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9913 9914 return CDRF_NEWFONT; 9915 default: 9916 return 0; 9917 9918 } 9919 case NM_RETURN: // no need since i subclass keydown 9920 break; 9921 case LVN_COLUMNCLICK: 9922 auto info = cast(LPNMLISTVIEW) hdr; 9923 this.emit!HeaderClickedEvent(info.iSubItem); 9924 break; 9925 case NM_CLICK: 9926 case NM_DBLCLK: 9927 case NM_RCLICK: 9928 case NM_RDBLCLK: 9929 // the item/subitem is set here and that can be a useful notification 9930 // even beyond the normal click notification 9931 break; 9932 case LVN_GETDISPINFO: 9933 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9934 if(info.item.mask & LVIF_TEXT) { 9935 if(getData) { 9936 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9937 auto bfr = WCharzBuffer(dataReceived); 9938 auto len = info.item.cchTextMax; 9939 if(bfr.length < len) 9940 len = cast(typeof(len)) bfr.length; 9941 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9942 info.item.pszText[len] = 0; 9943 }); 9944 } else { 9945 info.item.pszText[0] = 0; 9946 } 9947 //info.item.iItem 9948 //if(info.item.iSubItem) 9949 } 9950 break; 9951 default: 9952 } 9953 return 0; 9954 } 9955 9956 override bool encapsulatedChildren() { 9957 return true; 9958 } 9959 9960 /++ 9961 Informs the control that content has changed. 9962 9963 History: 9964 Added November 10, 2021 (dub v10.4) 9965 +/ 9966 void update() { 9967 version(custom_widgets) 9968 redraw(); 9969 else { 9970 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9971 UpdateWindow(hwnd); 9972 } 9973 9974 9975 } 9976 9977 /++ 9978 Called by the system to request the text content of an individual cell. You 9979 should pass the text into the provided `sink` delegate. This function will be 9980 called for each visible cell as-needed when drawing. 9981 +/ 9982 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9983 9984 /++ 9985 Available per-cell style customization options. Use one of the constructors 9986 provided to set the values conveniently, or default construct it and set individual 9987 values yourself. Just remember to set the `flags` so your values are actually used. 9988 If the flag isn't set, the field is ignored and the system default is used instead. 9989 9990 This is returned by the [getCellStyle] delegate. 9991 9992 Examples: 9993 --- 9994 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9995 auto table = new TableView(window); 9996 // snip: you would set up columns here 9997 9998 // this is how you provide data to the table view class 9999 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 10000 import std.conv; 10001 sink(to!string(my_data[row][column])); 10002 }; 10003 10004 // and this is how you customize the colors 10005 table.getCellStyle = delegate(int row, int column) { 10006 return (my_data[row][column] < 0) ? 10007 TableView.CellStyle(Color.red); // make negative numbers red 10008 : TableView.CellStyle.init; // leave the rest alone 10009 }; 10010 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 10011 --- 10012 10013 History: 10014 Added November 27, 2021 (dub v10.4) 10015 +/ 10016 struct CellStyle { 10017 /// Sets just a custom text color, leaving the background as the default. Use caution with certain colors as it may have illeglible contrast on the (unknown to you) background color. 10018 this(Color textColor) { 10019 this.textColor = textColor; 10020 this.flags |= Flags.textColorSet; 10021 } 10022 /// Sets a custom text and background color. 10023 this(Color textColor, Color backgroundColor) { 10024 this.textColor = textColor; 10025 this.backgroundColor = backgroundColor; 10026 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 10027 } 10028 10029 Color textColor; 10030 Color backgroundColor; 10031 int flags; /// bitmask of [Flags] 10032 /// available options to combine into [flags] 10033 enum Flags { 10034 textColorSet = 1 << 0, 10035 backgroundColorSet = 1 << 1, 10036 } 10037 } 10038 /++ 10039 Companion delegate to [getData] that allows you to custom style each 10040 cell of the table. 10041 10042 Returns: 10043 A [CellStyle] structure that describes the desired style for the 10044 given cell. `return CellStyle.init` if you want the default style. 10045 10046 History: 10047 Added November 27, 2021 (dub v10.4) 10048 +/ 10049 CellStyle delegate(int row, int column) getCellStyle; 10050 10051 // i want to be able to do things like draw little colored things to show red for negative numbers 10052 // or background color indicators or even in-cell charts 10053 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 10054 10055 /++ 10056 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 10057 +/ 10058 mixin Emits!HeaderClickedEvent; 10059 } 10060 10061 /++ 10062 This is emitted by the [TableView] when a user clicks on a column header. 10063 10064 Its member `columnIndex` has the zero-based index of the column that was clicked. 10065 10066 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 10067 10068 History: 10069 Added November 27, 2021 (dub v10.4) 10070 +/ 10071 class HeaderClickedEvent : Event { 10072 enum EventString = "HeaderClicked"; 10073 this(Widget target, int columnIndex) { 10074 this.columnIndex = columnIndex; 10075 super(EventString, target); 10076 } 10077 10078 /// The index of the column 10079 int columnIndex; 10080 10081 /// 10082 override @property int intValue() { 10083 return columnIndex; 10084 } 10085 } 10086 10087 version(custom_widgets) 10088 private class TableViewWidgetInner : Widget { 10089 10090 // wrap this thing in a ScrollMessageWidget 10091 10092 TableView tvw; 10093 ScrollMessageWidget smw; 10094 HeaderWidget header; 10095 10096 this(TableView tvw, ScrollMessageWidget smw) { 10097 this.tvw = tvw; 10098 this.smw = smw; 10099 super(smw); 10100 10101 this.tabStop = true; 10102 10103 header = new HeaderWidget(this, smw.getHeader()); 10104 10105 smw.addEventListener("scroll", () { 10106 this.redraw(); 10107 header.redraw(); 10108 }); 10109 10110 10111 // I need headers outside the scroll area but rendered on the same line as the up arrow 10112 // FIXME: add a fixed header to the SMW 10113 } 10114 10115 enum padding = 3; 10116 10117 void updateScrolls() { 10118 int w; 10119 foreach(idx, column; tvw.columns) { 10120 if(column.width == 0) continue; 10121 w += tvw.getActualSetSize(idx, false);// + padding; 10122 } 10123 smw.setTotalArea(w, tvw.itemCount); 10124 columnsWidth = w; 10125 } 10126 10127 private int columnsWidth; 10128 10129 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 10130 10131 override void registerMovement() { 10132 super.registerMovement(); 10133 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 10134 smw.setViewableArea(this.width, this.height / lh); 10135 } 10136 10137 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 10138 int x; 10139 int y; 10140 10141 int row = smw.position.y; 10142 10143 foreach(lol; 0 .. this.height / lh) { 10144 if(row >= tvw.itemCount) 10145 break; 10146 x = 0; 10147 foreach(columnNumber, column; tvw.columns) { 10148 auto x2 = x + column.calculatedWidth; 10149 auto smwx = smw.position.x; 10150 10151 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 10152 auto startX = x; 10153 auto endX = x + column.calculatedWidth; 10154 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 10155 case TextAlignment.Left: startX += padding; break; 10156 case TextAlignment.Center: startX += padding; endX -= padding; break; 10157 case TextAlignment.Right: endX -= padding; break; 10158 default: /* broken */ break; 10159 } 10160 if(column.width != 0) // no point drawing an invisible column 10161 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 10162 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh))); 10163 10164 void dotext(WidgetPainter painter) { 10165 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 10166 } 10167 10168 if(tvw.getCellStyle !is null) { 10169 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 10170 10171 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 10172 auto tempPainter = painter; 10173 tempPainter.fillColor = style.backgroundColor; 10174 tempPainter.outlineColor = style.backgroundColor; 10175 10176 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 10177 Point(endX - smw.position.x, y + lh)); 10178 } 10179 auto tempPainter = painter; 10180 if(style.flags & TableView.CellStyle.Flags.textColorSet) 10181 tempPainter.outlineColor = style.textColor; 10182 10183 dotext(tempPainter); 10184 } else { 10185 dotext(painter); 10186 } 10187 }); 10188 } 10189 10190 x += column.calculatedWidth; 10191 } 10192 row++; 10193 y += lh; 10194 } 10195 return bounds; 10196 } 10197 10198 static class Style : Widget.Style { 10199 override WidgetBackground background() { 10200 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 10201 } 10202 } 10203 mixin OverrideStyle!Style; 10204 10205 private static class HeaderWidget : Widget { 10206 /+ 10207 maybe i should do a splitter thing on top of the other widgets 10208 so the splitter itself isn't really drawn but still replies to mouse events? 10209 +/ 10210 this(TableViewWidgetInner tvw, Widget parent) { 10211 super(parent); 10212 this.tvw = tvw; 10213 10214 this.remainder = new Button("", this); 10215 10216 this.addEventListener((scope ClickEvent ev) { 10217 int header = -1; 10218 foreach(idx, child; this.children[1 .. $]) { 10219 if(child is ev.target) { 10220 header = cast(int) idx; 10221 break; 10222 } 10223 } 10224 10225 if(header != -1) { 10226 auto hce = new HeaderClickedEvent(tvw.tvw, header); 10227 hce.dispatch(); 10228 } 10229 10230 }); 10231 } 10232 10233 void updateHeaders() { 10234 foreach(child; children[1 .. $]) 10235 child.removeWidget(); 10236 10237 foreach(column; tvw.tvw.columns) { 10238 // the cast is ok because I dup it above, just the type is never changed. 10239 // all this is private so it should never get messed up. 10240 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 10241 } 10242 } 10243 10244 Button remainder; 10245 TableViewWidgetInner tvw; 10246 10247 override void recomputeChildLayout() { 10248 registerMovement(); 10249 int pos; 10250 foreach(idx, child; children[1 .. $]) { 10251 if(idx >= tvw.tvw.columns.length) 10252 continue; 10253 child.x = pos; 10254 child.y = 0; 10255 child.width = tvw.tvw.columns[idx].calculatedWidth; 10256 child.height = scaleWithDpi(16);// this.height; 10257 pos += child.width; 10258 10259 child.recomputeChildLayout(); 10260 } 10261 10262 if(remainder is null) 10263 return; 10264 10265 remainder.x = pos; 10266 remainder.y = 0; 10267 if(pos < this.width) 10268 remainder.width = this.width - pos;// + 4; 10269 else 10270 remainder.width = 0; 10271 remainder.height = scaleWithDpi(16); 10272 10273 remainder.recomputeChildLayout(); 10274 } 10275 10276 // for the scrollable children mixin 10277 Point scrollOrigin() { 10278 return Point(tvw.smw.position.x, 0); 10279 } 10280 void paintFrameAndBackground(WidgetPainter painter) { } 10281 10282 mixin ScrollableChildren; 10283 } 10284 } 10285 10286 /+ 10287 10288 // given struct / array / number / string / etc, make it viewable and editable 10289 class DataViewerWidget : Widget { 10290 10291 } 10292 +/ 10293 10294 /++ 10295 A line edit box with an associated label. 10296 10297 History: 10298 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 10299 10300 ``` 10301 Old: ________ 10302 10303 New: 10304 ____________ 10305 ``` 10306 10307 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 10308 10309 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 10310 horizontal label but left aligned. You may also consider a [GridLayout]. 10311 +/ 10312 alias LabeledLineEdit = Labeled!LineEdit; 10313 10314 private int widthThatWouldFitChildLabels(Widget w) { 10315 if(w is null) 10316 return 0; 10317 10318 int max; 10319 10320 if(auto label = cast(TextLabel) w) { 10321 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 10322 } else { 10323 foreach(child; w.children) { 10324 max = mymax(max, widthThatWouldFitChildLabels(child)); 10325 } 10326 } 10327 10328 return max; 10329 } 10330 10331 /++ 10332 History: 10333 Added May 19, 2021 10334 +/ 10335 class Labeled(T) : Widget { 10336 /// 10337 this(string label, Widget parent) { 10338 super(parent); 10339 initialize!VerticalLayout(label, TextAlignment.Left, parent); 10340 } 10341 10342 /++ 10343 History: 10344 The alignment parameter was added May 17, 2021 10345 +/ 10346 this(string label, TextAlignment alignment, Widget parent) { 10347 super(parent); 10348 initialize!HorizontalLayout(label, alignment, parent); 10349 } 10350 10351 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 10352 tabStop = false; 10353 horizontal = is(L == HorizontalLayout); 10354 auto hl = new L(this); 10355 if(horizontal) { 10356 static class SpecialTextLabel : TextLabel { 10357 Widget outerParent; 10358 10359 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 10360 this.outerParent = outerParent; 10361 super(label, alignment, parent); 10362 } 10363 10364 override int flexBasisWidth() { 10365 return widthThatWouldFitChildLabels(outerParent); 10366 } 10367 /+ 10368 override int widthShrinkiness() { return 0; } 10369 override int widthStretchiness() { return 1; } 10370 +/ 10371 10372 override int paddingRight() { return 6; } 10373 override int paddingLeft() { return 9; } 10374 10375 override int paddingTop() { return 3; } 10376 } 10377 this.label = new SpecialTextLabel(label, alignment, parent, hl); 10378 } else 10379 this.label = new TextLabel(label, alignment, hl); 10380 this.lineEdit = new T(hl); 10381 10382 this.label.labelFor = this.lineEdit; 10383 } 10384 10385 private bool horizontal; 10386 10387 TextLabel label; /// 10388 T lineEdit; /// 10389 10390 override int flexBasisWidth() { return 250; } 10391 override int widthShrinkiness() { return 1; } 10392 10393 override int minHeight() { 10394 return this.children[0].minHeight; 10395 } 10396 override int maxHeight() { return minHeight(); } 10397 override int marginTop() { return 4; } 10398 override int marginBottom() { return 4; } 10399 10400 // FIXME: i should prolly call it value as well as content tbh 10401 10402 /// 10403 @property string content() { 10404 return lineEdit.content; 10405 } 10406 /// 10407 @property void content(string c) { 10408 return lineEdit.content(c); 10409 } 10410 10411 /// 10412 void selectAll() { 10413 lineEdit.selectAll(); 10414 } 10415 10416 override void focus() { 10417 lineEdit.focus(); 10418 } 10419 } 10420 10421 /++ 10422 A labeled password edit. 10423 10424 History: 10425 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 10426 10427 The default parameters for the constructors were also removed on May 19, 2021 10428 +/ 10429 alias LabeledPasswordEdit = Labeled!PasswordEdit; 10430 10431 private string toMenuLabel(string s) { 10432 string n; 10433 n.reserve(s.length); 10434 foreach(c; s) 10435 if(c == '_') 10436 n ~= ' '; 10437 else 10438 n ~= c; 10439 return n; 10440 } 10441 10442 private void autoExceptionHandler(Exception e) { 10443 messageBox(e.msg); 10444 } 10445 10446 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) { 10447 static if(is(T : void delegate())) { 10448 return () { 10449 try 10450 t(); 10451 catch(Exception e) 10452 autoExceptionHandler(e); 10453 }; 10454 } else static if(is(typeof(fn) Params == __parameters)) { 10455 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 10456 return () { 10457 void onOK(string s) { 10458 member = s; 10459 try 10460 t(Params[0](s)); 10461 catch(Exception e) 10462 autoExceptionHandler(e); 10463 } 10464 10465 if( 10466 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 10467 || type == FileDialogType.Save) 10468 { 10469 getSaveFileName(window, &onOK, member, filters, null); 10470 } else 10471 getOpenFileName(window, &onOK, member, filters, null); 10472 }; 10473 } else { 10474 struct S { 10475 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 10476 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 10477 } else mixin(q{ 10478 static foreach(idx, ignore; Params) { 10479 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 10480 } 10481 }); 10482 } 10483 return () { 10484 dialog(window, (S s) { 10485 try { 10486 static if(is(typeof(t) Ret == return)) { 10487 static if(is(Ret == void)) { 10488 t(s.tupleof); 10489 } else { 10490 auto ret = t(s.tupleof); 10491 import std.conv; 10492 messageBox(to!string(ret), "Returned Value"); 10493 } 10494 } 10495 } catch(Exception e) 10496 autoExceptionHandler(e); 10497 }, null, __traits(identifier, fn)); 10498 }; 10499 } 10500 } 10501 } 10502 10503 private template hasAnyRelevantAnnotations(a...) { 10504 bool helper() { 10505 bool any; 10506 foreach(attr; a) { 10507 static if(is(typeof(attr) == .menu)) 10508 any = true; 10509 else static if(is(typeof(attr) == .toolbar)) 10510 any = true; 10511 else static if(is(attr == .separator)) 10512 any = true; 10513 else static if(is(typeof(attr) == .accelerator)) 10514 any = true; 10515 else static if(is(typeof(attr) == .hotkey)) 10516 any = true; 10517 else static if(is(typeof(attr) == .icon)) 10518 any = true; 10519 else static if(is(typeof(attr) == .label)) 10520 any = true; 10521 else static if(is(typeof(attr) == .tip)) 10522 any = true; 10523 } 10524 return any; 10525 } 10526 10527 enum bool hasAnyRelevantAnnotations = helper(); 10528 } 10529 10530 /++ 10531 A `MainWindow` is a window that includes turnkey support for a menu bar, tool bar, and status bar automatically positioned around a client area where you put your widgets. 10532 +/ 10533 class MainWindow : Window { 10534 /// 10535 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 10536 super(initialWidth, initialHeight, title); 10537 10538 _clientArea = new ClientAreaWidget(); 10539 _clientArea.x = 0; 10540 _clientArea.y = 0; 10541 _clientArea.width = this.width; 10542 _clientArea.height = this.height; 10543 _clientArea.tabStop = false; 10544 10545 super.addChild(_clientArea); 10546 10547 statusBar = new StatusBar(this); 10548 } 10549 10550 /++ 10551 Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though). 10552 10553 The only required annotation on a function is `@menu("Label")` to make it appear, but there are several optional ones I'd recommend considering, including `@toolbar("group name")`, `@icon()`, `@accelerator("keyboard shortcut string")`, and `@hotkey('char')`. 10554 10555 You can also use `@separator` to put a separating line in the menu before the function. 10556 10557 Functions may have zero or one argument. If they have an argument, an automatic dialog box (see: [dialog]) will be created to request the data from the user before calling your function. Some types have special treatment, like [FileName], will invoke the file dialog, assuming open or save based on the name of your function. 10558 10559 Let's look at a complete example: 10560 10561 --- 10562 import arsd.minigui; 10563 10564 void main() { 10565 auto window = new MainWindow(); 10566 10567 // we can add widgets before or after setting the menu, either way is fine. 10568 // i'll do it before here so the local variables are available to the commands. 10569 10570 auto textEdit = new TextEdit(window); 10571 10572 // Remember, in D, you can define structs inside of functions 10573 // and those structs can access the function's local variables. 10574 // 10575 // Of course, you might also want to do this separately, and if you 10576 // do, make sure you keep a reference to the window as a struct data 10577 // member so you can refer to it in cases like this Exit function. 10578 struct Commands { 10579 // the & in the string indicates that the next letter is the hotkey 10580 // to access it from the keyboard (so here, alt+f will open the 10581 // file menu) 10582 @menu("&File") { 10583 @accelerator("Ctrl+N") 10584 @hotkey('n') 10585 @icon(GenericIcons.New) // add an icon to the action 10586 @toolbar("File") // adds it to a toolbar. 10587 // The toolbar name is never visible to the user, but is used to group icons. 10588 void New() { 10589 previousFileReferenced = null; 10590 textEdit.content = ""; 10591 } 10592 10593 @icon(GenericIcons.Open) 10594 @toolbar("File") 10595 @hotkey('s') 10596 @accelerator("Ctrl+O") 10597 void Open(FileName!() filename) { 10598 import std.file; 10599 textEdit.content = std.file.readText(filename); 10600 } 10601 10602 @icon(GenericIcons.Save) 10603 @toolbar("File") 10604 @accelerator("Ctrl+S") 10605 @hotkey('s') 10606 void Save() { 10607 // these are still functions, so of course you can 10608 // still call them yourself too 10609 Save_As(previousFileReferenced); 10610 } 10611 10612 // underscores translate to spaces in the visible name 10613 @hotkey('a') 10614 void Save_As(FileName!() filename) { 10615 import std.file; 10616 std.file.write(previousFileReferenced, textEdit.content); 10617 } 10618 10619 // you can put the annotations before or after the function name+args and it works the same way 10620 @separator 10621 void Exit() @accelerator("Alt+F4") @hotkey('x') { 10622 window.close(); 10623 } 10624 } 10625 10626 @menu("&Edit") { 10627 // not putting accelerators here because the text edit widget 10628 // does it locally, so no need to duplicate it globally. 10629 10630 @icon(GenericIcons.Undo) 10631 void Undo() @toolbar("Undo") { 10632 textEdit.undo(); 10633 } 10634 10635 @separator 10636 10637 @icon(GenericIcons.Cut) 10638 void Cut() @toolbar("Edit") { 10639 textEdit.cut(); 10640 } 10641 @icon(GenericIcons.Copy) 10642 void Copy() @toolbar("Edit") { 10643 textEdit.copy(); 10644 } 10645 @icon(GenericIcons.Paste) 10646 void Paste() @toolbar("Edit") { 10647 textEdit.paste(); 10648 } 10649 10650 @separator 10651 void Select_All() { 10652 textEdit.selectAll(); 10653 } 10654 } 10655 10656 @menu("Help") { 10657 void About() @accelerator("F1") { 10658 window.messageBox("A minigui sample program."); 10659 } 10660 10661 // @label changes the name in the menu from what is in the code 10662 @label("In Menu Name") 10663 void otherNameInCode() {} 10664 } 10665 } 10666 10667 // declare the object that holds the commands, and set 10668 // and members you want from it 10669 Commands commands; 10670 10671 // and now tell minigui to do its magic and create the ui for it! 10672 window.setMenuAndToolbarFromAnnotatedCode(commands); 10673 10674 // then, loop the window normally; 10675 window.loop(); 10676 10677 // important to note that the `commands` variable must live through the window's whole life cycle, 10678 // or you can have crashes. If you declare the variable and loop in different functions, make sure 10679 // you do `new Commands` so the garbage collector can take over management of it for you. 10680 } 10681 --- 10682 10683 Note that you can call this function multiple times and it will add the items in order to the given items. 10684 10685 +/ 10686 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 10687 setMenuAndToolbarFromAnnotatedCode_internal(t); 10688 } 10689 /// ditto 10690 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 10691 setMenuAndToolbarFromAnnotatedCode_internal(t); 10692 } 10693 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 10694 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 10695 Menu[string] mcs; 10696 10697 alias ToolbarSection = ToolBar.ToolbarSection; 10698 ToolbarSection[] toolbarSections; 10699 10700 foreach(menu; menuBar.subMenus) { 10701 mcs[menu.label] = menu; 10702 } 10703 10704 foreach(memberName; __traits(derivedMembers, T)) { 10705 static if(memberName != "this") 10706 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 10707 .menu menu; 10708 .toolbar toolbar; 10709 bool separator; 10710 .accelerator accelerator; 10711 .hotkey hotkey; 10712 .icon icon; 10713 string label; 10714 string tip; 10715 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 10716 static if(is(typeof(attr) == .menu)) 10717 menu = attr; 10718 else static if(is(typeof(attr) == .toolbar)) 10719 toolbar = attr; 10720 else static if(is(attr == .separator)) 10721 separator = true; 10722 else static if(is(typeof(attr) == .accelerator)) 10723 accelerator = attr; 10724 else static if(is(typeof(attr) == .hotkey)) 10725 hotkey = attr; 10726 else static if(is(typeof(attr) == .icon)) 10727 icon = attr; 10728 else static if(is(typeof(attr) == .label)) 10729 label = attr.label; 10730 else static if(is(typeof(attr) == .tip)) 10731 tip = attr.tip; 10732 } 10733 10734 if(menu !is .menu.init || toolbar !is .toolbar.init) { 10735 ushort correctIcon = icon.id; // FIXME 10736 if(label.length == 0) 10737 label = memberName.toMenuLabel; 10738 10739 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName)); 10740 10741 auto action = new Action(label, correctIcon, handler); 10742 10743 if(accelerator.keyString.length) { 10744 auto ke = KeyEvent.parse(accelerator.keyString); 10745 action.accelerator = ke; 10746 accelerators[ke.toStr] = handler; 10747 } 10748 10749 if(toolbar !is .toolbar.init) { 10750 bool found; 10751 foreach(ref section; toolbarSections) 10752 if(section.name == toolbar.groupName) { 10753 section.actions ~= action; 10754 found = true; 10755 break; 10756 } 10757 if(!found) { 10758 toolbarSections ~= ToolbarSection(toolbar.groupName, [action]); 10759 } 10760 } 10761 if(menu !is .menu.init) { 10762 Menu mc; 10763 if(menu.name in mcs) { 10764 mc = mcs[menu.name]; 10765 } else { 10766 mc = new Menu(menu.name, this); 10767 menuBar.addItem(mc); 10768 mcs[menu.name] = mc; 10769 } 10770 10771 if(separator) 10772 mc.addSeparator(); 10773 auto mi = mc.addItem(new MenuItem(action)); 10774 10775 if(hotkey !is .hotkey.init) 10776 mi.hotkey = hotkey.ch; 10777 } 10778 } 10779 } 10780 } 10781 10782 this.menuBar = menuBar; 10783 10784 if(toolbarSections.length) { 10785 auto tb = new ToolBar(toolbarSections, this); 10786 } 10787 } 10788 10789 void delegate()[string] accelerators; 10790 10791 override void defaultEventHandler_keydown(KeyDownEvent event) { 10792 auto str = event.originalKeyEvent.toStr; 10793 if(auto acl = str in accelerators) 10794 (*acl)(); 10795 10796 // Windows this this automatically so only on custom need we implement it 10797 version(custom_widgets) { 10798 if(event.altKey && this.menuBar) { 10799 foreach(item; this.menuBar.items) { 10800 if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) { 10801 // FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code 10802 item.dynamicState = DynamicState.hover | DynamicState.depressed; 10803 item.redraw(); 10804 auto e = new MouseDownEvent(item); 10805 e.dispatch(); 10806 break; 10807 } 10808 } 10809 } 10810 10811 if(event.key == Key.Menu) { 10812 showContextMenu(-1, -1); 10813 } 10814 } 10815 10816 super.defaultEventHandler_keydown(event); 10817 } 10818 10819 override void defaultEventHandler_mouseover(MouseOverEvent event) { 10820 super.defaultEventHandler_mouseover(event); 10821 if(this.statusBar !is null && event.target.statusTip.length) 10822 this.statusBar.parts[0].content = event.target.statusTip; 10823 else if(this.statusBar !is null && this.statusTip.length) 10824 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 10825 } 10826 10827 override void addChild(Widget c, int position = int.max) { 10828 if(auto tb = cast(ToolBar) c) 10829 version(win32_widgets) 10830 super.addChild(c, 0); 10831 else version(custom_widgets) 10832 super.addChild(c, menuBar ? 1 : 0); 10833 else static assert(0); 10834 else 10835 clientArea.addChild(c, position); 10836 } 10837 10838 ToolBar _toolBar; 10839 /// 10840 ToolBar toolBar() { return _toolBar; } 10841 /// 10842 ToolBar toolBar(ToolBar t) { 10843 _toolBar = t; 10844 foreach(child; this.children) 10845 if(child is t) 10846 return t; 10847 version(win32_widgets) 10848 super.addChild(t, 0); 10849 else version(custom_widgets) 10850 super.addChild(t, menuBar ? 1 : 0); 10851 else static assert(0); 10852 return t; 10853 } 10854 10855 MenuBar _menu; 10856 /// 10857 MenuBar menuBar() { return _menu; } 10858 /// 10859 MenuBar menuBar(MenuBar m) { 10860 if(m is _menu) { 10861 version(custom_widgets) 10862 queueRecomputeChildLayout(); 10863 return m; 10864 } 10865 10866 if(_menu !is null) { 10867 // make sure it is sanely removed 10868 // FIXME 10869 } 10870 10871 _menu = m; 10872 10873 version(win32_widgets) { 10874 SetMenu(parentWindow.win.impl.hwnd, m.handle); 10875 } else version(custom_widgets) { 10876 super.addChild(m, 0); 10877 10878 // clientArea.y = menu.height; 10879 // clientArea.height = this.height - menu.height; 10880 10881 queueRecomputeChildLayout(); 10882 } else static assert(false); 10883 10884 return _menu; 10885 } 10886 private Widget _clientArea; 10887 /// 10888 @property Widget clientArea() { return _clientArea; } 10889 protected @property void clientArea(Widget wid) { 10890 _clientArea = wid; 10891 } 10892 10893 private StatusBar _statusBar; 10894 /++ 10895 Returns the window's [StatusBar]. Be warned it may be `null`. 10896 +/ 10897 @property StatusBar statusBar() { return _statusBar; } 10898 /// ditto 10899 @property void statusBar(StatusBar bar) { 10900 if(_statusBar !is null) 10901 _statusBar.removeWidget(); 10902 _statusBar = bar; 10903 if(bar !is null) 10904 super.addChild(_statusBar); 10905 } 10906 } 10907 10908 /+ 10909 This is really an implementation detail of [MainWindow] 10910 +/ 10911 private class ClientAreaWidget : Widget { 10912 this() { 10913 this.tabStop = false; 10914 super(null); 10915 //sa = new ScrollableWidget(this); 10916 } 10917 /* 10918 ScrollableWidget sa; 10919 override void addChild(Widget w, int position) { 10920 if(sa is null) 10921 super.addChild(w, position); 10922 else { 10923 sa.addChild(w, position); 10924 sa.setContentSize(this.minWidth + 1, this.minHeight); 10925 writeln(sa.contentWidth, "x", sa.contentHeight); 10926 } 10927 } 10928 */ 10929 } 10930 10931 /** 10932 Toolbars are lists of buttons (typically icons) that appear under the menu. 10933 Each button ought to correspond to a menu item, represented by [Action] objects. 10934 */ 10935 class ToolBar : Widget { 10936 version(win32_widgets) { 10937 private int idealHeight; 10938 override int minHeight() { return idealHeight; } 10939 override int maxHeight() { return idealHeight; } 10940 } else version(custom_widgets) { 10941 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10942 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10943 } else static assert(false); 10944 override int heightStretchiness() { return 0; } 10945 10946 static struct ToolbarSection { 10947 string name; 10948 Action[] actions; 10949 } 10950 10951 version(win32_widgets) { 10952 HIMAGELIST imageListSmall; 10953 HIMAGELIST imageListLarge; 10954 } 10955 10956 this(Widget parent) { 10957 this(cast(ToolbarSection[]) null, parent); 10958 } 10959 10960 version(win32_widgets) 10961 void changeIconSize(bool useLarge) { 10962 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10963 10964 /+ 10965 SIZE size; 10966 import core.sys.windows.commctrl; 10967 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10968 idealHeight = size.cy + 4; // the plus 4 is a hack 10969 +/ 10970 10971 idealHeight = useLarge ? 34 : 26; 10972 10973 if(parent) { 10974 parent.queueRecomputeChildLayout(); 10975 parent.redraw(); 10976 } 10977 10978 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10979 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10980 } 10981 10982 /++ 10983 History: 10984 The `ToolbarSection` overload was added December 31, 2024 10985 +/ 10986 this(Action[] actions, Widget parent) { 10987 this([ToolbarSection(null, actions)], parent); 10988 } 10989 10990 /// ditto 10991 this(ToolbarSection[] sections, Widget parent) { 10992 super(parent); 10993 10994 tabStop = false; 10995 10996 version(win32_widgets) { 10997 // so i like how the flat thing looks on windows, but not on wine 10998 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10999 // leave it commented 11000 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 11001 11002 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 11003 11004 imageListSmall = ImageList_Create( 11005 // width, height 11006 16, 16, 11007 ILC_COLOR16 | ILC_MASK, 11008 16 /*numberOfButtons*/, 0); 11009 11010 imageListLarge = ImageList_Create( 11011 // width, height 11012 24, 24, 11013 ILC_COLOR16 | ILC_MASK, 11014 16 /*numberOfButtons*/, 0); 11015 11016 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 11017 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 11018 11019 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 11020 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 11021 11022 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 11023 11024 TBBUTTON[] buttons; 11025 11026 // FIXME: I_IMAGENONE is if here is no icon 11027 foreach(sidx, section; sections) { 11028 if(sidx) 11029 buttons ~= TBBUTTON( 11030 scaleWithDpi(4), 11031 0, 11032 TBSTATE_ENABLED, // state 11033 TBSTYLE_SEP | BTNS_SEP, // style 11034 0, // reserved array, just zero it out 11035 0, // dwData 11036 -1 11037 ); 11038 11039 foreach(action; section.actions) 11040 buttons ~= TBBUTTON( 11041 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 11042 action.id, 11043 TBSTATE_ENABLED, // state 11044 0, // style 11045 0, // reserved array, just zero it out 11046 0, // dwData 11047 cast(size_t) toWstringzInternal(action.label) // INT_PTR 11048 ); 11049 } 11050 11051 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 11052 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 11053 11054 /* 11055 RECT rect; 11056 GetWindowRect(hwnd, &rect); 11057 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 11058 */ 11059 11060 dpiChanged(); // to load the things calling changeIconSize the first time 11061 11062 assert(idealHeight); 11063 } else version(custom_widgets) { 11064 foreach(sidx, section; sections) { 11065 if(sidx) 11066 new HorizontalSpacer(4, this); 11067 foreach(action; section.actions) 11068 new ToolButton(action, this); 11069 } 11070 } else static assert(false); 11071 } 11072 11073 override void recomputeChildLayout() { 11074 .recomputeChildLayout!"width"(this); 11075 } 11076 11077 11078 version(win32_widgets) 11079 override protected void dpiChanged() { 11080 auto sz = scaleWithDpi(16); 11081 if(sz >= 20) 11082 changeIconSize(true); 11083 else 11084 changeIconSize(false); 11085 } 11086 } 11087 11088 enum toolbarIconSize = 24; 11089 11090 /// An implementation helper for [ToolBar]. Generally, you shouldn't create these yourself and instead just pass [Action]s to [ToolBar]'s constructor and let it create the buttons for you. 11091 class ToolButton : Button { 11092 /// 11093 this(Action action, Widget parent) { 11094 super(action.label, parent); 11095 tabStop = false; 11096 this.action = action; 11097 } 11098 11099 version(custom_widgets) 11100 override void defaultEventHandler_click(ClickEvent event) { 11101 foreach(handler; action.triggered) 11102 handler(); 11103 } 11104 11105 Action action; 11106 11107 override int maxWidth() { return toolbarIconSize; } 11108 override int minWidth() { return toolbarIconSize; } 11109 override int maxHeight() { return toolbarIconSize; } 11110 override int minHeight() { return toolbarIconSize; } 11111 11112 version(custom_widgets) 11113 override void paint(WidgetPainter painter) { 11114 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 11115 painter.outlineColor = Color.black; 11116 11117 // I want to get from 16 to 24. that's * 3 / 2 11118 static assert(toolbarIconSize >= 16); 11119 enum multiplier = toolbarIconSize / 8; 11120 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 11121 switch(action.iconId) { 11122 case GenericIcons.New: 11123 painter.fillColor = Color.white; 11124 painter.drawPolygon( 11125 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 11126 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 11127 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 11128 ); 11129 break; 11130 case GenericIcons.Save: 11131 painter.fillColor = Color.white; 11132 painter.outlineColor = Color.black; 11133 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 11134 11135 // the label 11136 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 11137 11138 // the slider 11139 painter.fillColor = Color.black; 11140 painter.outlineColor = Color.black; 11141 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 11142 11143 painter.fillColor = Color.white; 11144 painter.outlineColor = Color.white; 11145 // the disc window 11146 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 11147 break; 11148 case GenericIcons.Open: 11149 painter.fillColor = Color.white; 11150 painter.drawPolygon( 11151 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 11152 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 11153 painter.drawPolygon( 11154 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 11155 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 11156 Point(2, 6) * multiplier / divisor); 11157 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 11158 break; 11159 case GenericIcons.Copy: 11160 painter.fillColor = Color.white; 11161 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 11162 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 11163 break; 11164 case GenericIcons.Cut: 11165 painter.fillColor = Color.transparent; 11166 painter.outlineColor = getComputedStyle.foregroundColor(); 11167 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 11168 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 11169 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 11170 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 11171 break; 11172 case GenericIcons.Paste: 11173 painter.fillColor = Color.white; 11174 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 11175 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 11176 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 11177 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 11178 painter.fillColor = Color.black; 11179 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 11180 break; 11181 case GenericIcons.Help: 11182 painter.outlineColor = getComputedStyle.foregroundColor(); 11183 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11184 break; 11185 case GenericIcons.Undo: 11186 painter.fillColor = Color.transparent; 11187 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 11188 painter.outlineColor = Color.black; 11189 painter.fillColor = Color.black; 11190 painter.drawPolygon( 11191 Point(4, 4) * multiplier / divisor, 11192 Point(8, 2) * multiplier / divisor, 11193 Point(8, 6) * multiplier / divisor, 11194 Point(4, 4) * multiplier / divisor, 11195 ); 11196 break; 11197 case GenericIcons.Redo: 11198 painter.fillColor = Color.transparent; 11199 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 11200 painter.outlineColor = Color.black; 11201 painter.fillColor = Color.black; 11202 painter.drawPolygon( 11203 Point(10, 4) * multiplier / divisor, 11204 Point(6, 2) * multiplier / divisor, 11205 Point(6, 6) * multiplier / divisor, 11206 Point(10, 4) * multiplier / divisor, 11207 ); 11208 break; 11209 default: 11210 painter.outlineColor = getComputedStyle.foregroundColor; 11211 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11212 } 11213 return bounds; 11214 }); 11215 } 11216 11217 } 11218 11219 11220 /++ 11221 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 11222 +/ 11223 class MenuBar : Widget { 11224 MenuItem[] items; 11225 Menu[] subMenus; 11226 11227 version(win32_widgets) { 11228 HMENU handle; 11229 /// 11230 this(Widget parent = null) { 11231 super(parent); 11232 11233 handle = CreateMenu(); 11234 tabStop = false; 11235 } 11236 } else version(custom_widgets) { 11237 /// 11238 this(Widget parent = null) { 11239 tabStop = false; // these are selected some other way 11240 super(parent); 11241 } 11242 11243 mixin Padding!q{2}; 11244 } else static assert(false); 11245 11246 version(custom_widgets) 11247 override void paint(WidgetPainter painter) { 11248 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 11249 } 11250 11251 /// 11252 MenuItem addItem(MenuItem item) { 11253 this.addChild(item); 11254 items ~= item; 11255 version(win32_widgets) { 11256 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11257 } 11258 return item; 11259 } 11260 11261 11262 /// 11263 Menu addItem(Menu item) { 11264 11265 subMenus ~= item; 11266 11267 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 11268 11269 addChild(mbItem); 11270 items ~= mbItem; 11271 11272 version(win32_widgets) { 11273 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 11274 } else version(custom_widgets) { 11275 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 11276 item.popup(mbItem); 11277 }; 11278 } else static assert(false); 11279 11280 return item; 11281 } 11282 11283 override void recomputeChildLayout() { 11284 .recomputeChildLayout!"width"(this); 11285 } 11286 11287 override int maxHeight() { return defaultLineHeight + 4; } 11288 override int minHeight() { return defaultLineHeight + 4; } 11289 } 11290 11291 11292 /** 11293 Status bars appear at the bottom of a MainWindow. 11294 They are made out of Parts, with a width and content. 11295 11296 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 11297 11298 11299 sb.parts[0].content = "Status bar text!"; 11300 */ 11301 class StatusBar : Widget { 11302 private Part[] partsArray; 11303 /// 11304 struct Parts { 11305 @disable this(); 11306 this(StatusBar owner) { this.owner = owner; } 11307 //@disable this(this); 11308 /// 11309 @property int length() { return cast(int) owner.partsArray.length; } 11310 private StatusBar owner; 11311 private this(StatusBar owner, Part[] parts) { 11312 this.owner.partsArray = parts; 11313 this.owner = owner; 11314 } 11315 /// 11316 Part opIndex(int p) { 11317 if(owner.partsArray.length == 0) 11318 this ~= new StatusBar.Part(0); 11319 return owner.partsArray[p]; 11320 } 11321 11322 /// 11323 Part opOpAssign(string op : "~" )(Part p) { 11324 assert(owner.partsArray.length < 255); 11325 p.owner = this.owner; 11326 p.idx = cast(int) owner.partsArray.length; 11327 owner.partsArray ~= p; 11328 11329 owner.queueRecomputeChildLayout(); 11330 11331 version(win32_widgets) { 11332 int[256] pos; 11333 int cpos; 11334 foreach(idx, part; owner.partsArray) { 11335 if(idx + 1 == owner.partsArray.length) 11336 pos[idx] = -1; 11337 else { 11338 cpos += part.currentlyAssignedWidth; 11339 pos[idx] = cpos; 11340 } 11341 } 11342 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 11343 } else version(custom_widgets) { 11344 owner.redraw(); 11345 } else static assert(false); 11346 11347 return p; 11348 } 11349 11350 /++ 11351 Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions. 11352 11353 No given item should be 0. 11354 11355 History: 11356 Added December 31, 2024 11357 +/ 11358 void setSizes(int[] proportions...) { 11359 assert(this.owner); 11360 this.owner.partsArray = null; 11361 11362 foreach(n; proportions) { 11363 assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead."); 11364 11365 this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels)); 11366 } 11367 11368 } 11369 } 11370 11371 private Parts _parts; 11372 /// 11373 final @property Parts parts() { 11374 return _parts; 11375 } 11376 11377 /++ 11378 11379 +/ 11380 static class Part { 11381 /++ 11382 History: 11383 Added September 1, 2023 (dub v11.1) 11384 +/ 11385 enum WidthUnits { 11386 /++ 11387 Unscaled pixels as they appear on screen. 11388 11389 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 11390 +/ 11391 DeviceDependentPixels, 11392 /++ 11393 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 11394 +/ 11395 DeviceIndependentPixels, 11396 /++ 11397 An approximate character count in the currently selected font (at layout time) of the status bar. This will use the x-width (similar to css `ch`). 11398 +/ 11399 ApproximateCharacters, 11400 /++ 11401 These take a proportion of the remaining space in the window after all other parts have been assigned. The sum of all proportional parts is then divided by the current item to get the amount of space it uses. 11402 11403 If you pass 0, it will assume that this item takes an average of all remaining proportional space. This is there primarily to provide compatibility with code written against older versions of minigui. 11404 +/ 11405 Proportional 11406 } 11407 private WidthUnits units; 11408 private int width; 11409 private StatusBar owner; 11410 11411 private int currentlyAssignedWidth; 11412 11413 /++ 11414 History: 11415 Prior to September 1, 2023, this took a default value of 100 and was interpreted as pixels, unless the value was 0 and it was the last item in the list, in which case it would use the remaining space in the window. 11416 11417 It now allows you to provide your own value for [WidthUnits]. 11418 11419 Additionally, the default value used to be an arbitrary value of 100. It is now 0, to take advantage of the automatic proportional calculator in the new version. If you want the old behavior, pass `100, StatusBar.Part.WidthUnits.DeviceIndependentPixels`. 11420 +/ 11421 this(int w, WidthUnits units = WidthUnits.Proportional) { 11422 this.units = units; 11423 this.width = w; 11424 } 11425 11426 /// ditto 11427 this(int w = 0) { 11428 if(w == 0) 11429 this(w, WidthUnits.Proportional); 11430 else 11431 this(w, WidthUnits.DeviceDependentPixels); 11432 } 11433 11434 private int idx; 11435 private string _content; 11436 /// 11437 @property string content() { return _content; } 11438 /// 11439 @property void content(string s) { 11440 version(win32_widgets) { 11441 _content = s; 11442 WCharzBuffer bfr = WCharzBuffer(s); 11443 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 11444 } else version(custom_widgets) { 11445 if(_content != s) { 11446 _content = s; 11447 owner.redraw(); 11448 } 11449 } else static assert(false); 11450 } 11451 } 11452 string simpleModeContent; 11453 bool inSimpleMode; 11454 11455 11456 /// 11457 this(Widget parent) { 11458 super(null); // FIXME 11459 _parts = Parts(this); 11460 tabStop = false; 11461 version(win32_widgets) { 11462 parentWindow = parent.parentWindow; 11463 createWin32Window(this, "msctls_statusbar32"w, "", 0); 11464 11465 RECT rect; 11466 GetWindowRect(hwnd, &rect); 11467 idealHeight = rect.bottom - rect.top; 11468 assert(idealHeight); 11469 } else version(custom_widgets) { 11470 } else static assert(false); 11471 } 11472 11473 override void recomputeChildLayout() { 11474 int remainingLength = this.width; 11475 11476 int proportionalSum; 11477 int proportionalCount; 11478 foreach(idx, part; this.partsArray) { 11479 with(Part.WidthUnits) 11480 final switch(part.units) { 11481 case DeviceDependentPixels: 11482 part.currentlyAssignedWidth = part.width; 11483 remainingLength -= part.currentlyAssignedWidth; 11484 break; 11485 case DeviceIndependentPixels: 11486 part.currentlyAssignedWidth = scaleWithDpi(part.width); 11487 remainingLength -= part.currentlyAssignedWidth; 11488 break; 11489 case ApproximateCharacters: 11490 auto cs = getComputedStyle(); 11491 auto font = cs.font; 11492 11493 part.currentlyAssignedWidth = font.averageWidth * this.width; 11494 remainingLength -= part.currentlyAssignedWidth; 11495 break; 11496 case Proportional: 11497 proportionalSum += part.width; 11498 proportionalCount ++; 11499 break; 11500 } 11501 } 11502 11503 foreach(part; this.partsArray) { 11504 if(part.units == Part.WidthUnits.Proportional) { 11505 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 11506 if(proportion == 0) 11507 proportion = 1; 11508 11509 if(proportionalSum == 0) 11510 proportionalSum = proportionalCount; 11511 11512 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 11513 } 11514 } 11515 11516 super.recomputeChildLayout(); 11517 } 11518 11519 version(win32_widgets) 11520 override protected void dpiChanged() { 11521 RECT rect; 11522 GetWindowRect(hwnd, &rect); 11523 idealHeight = rect.bottom - rect.top; 11524 assert(idealHeight); 11525 } 11526 11527 version(custom_widgets) 11528 override void paint(WidgetPainter painter) { 11529 auto cs = getComputedStyle(); 11530 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11531 int cpos = 0; 11532 foreach(idx, part; this.partsArray) { 11533 auto partWidth = part.currentlyAssignedWidth; 11534 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 11535 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 11536 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 11537 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 11538 11539 painter.outlineColor = cs.foregroundColor(); 11540 painter.fillColor = cs.foregroundColor(); 11541 11542 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 11543 cpos += partWidth; 11544 } 11545 } 11546 11547 11548 version(win32_widgets) { 11549 private int idealHeight; 11550 override int maxHeight() { return idealHeight; } 11551 override int minHeight() { return idealHeight; } 11552 } else version(custom_widgets) { 11553 override int maxHeight() { return defaultLineHeight + 4; } 11554 override int minHeight() { return defaultLineHeight + 4; } 11555 } else static assert(false); 11556 } 11557 11558 /// Displays an in-progress indicator without known values 11559 version(none) 11560 class IndefiniteProgressBar : Widget { 11561 version(win32_widgets) 11562 this(Widget parent) { 11563 super(parent); 11564 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 11565 tabStop = false; 11566 } 11567 override int minHeight() { return 10; } 11568 } 11569 11570 /// A progress bar with a known endpoint and completion amount 11571 class ProgressBar : Widget { 11572 /++ 11573 History: 11574 Added March 16, 2022 (dub v10.7) 11575 +/ 11576 this(int min, int max, Widget parent) { 11577 this(parent); 11578 setRange(cast(ushort) min, cast(ushort) max); // FIXME 11579 } 11580 this(Widget parent) { 11581 version(win32_widgets) { 11582 super(parent); 11583 createWin32Window(this, "msctls_progress32"w, "", 0); 11584 tabStop = false; 11585 } else version(custom_widgets) { 11586 super(parent); 11587 max = 100; 11588 step = 10; 11589 tabStop = false; 11590 } else static assert(0); 11591 } 11592 11593 version(custom_widgets) 11594 override void paint(WidgetPainter painter) { 11595 auto cs = getComputedStyle(); 11596 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11597 painter.fillColor = cs.progressBarColor; 11598 painter.drawRectangle(Point(0, 0), width * current / max, height); 11599 } 11600 11601 11602 version(custom_widgets) { 11603 int current; 11604 int max; 11605 int step; 11606 } 11607 11608 /// 11609 void advanceOneStep() { 11610 version(win32_widgets) 11611 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 11612 else version(custom_widgets) 11613 addToPosition(step); 11614 else static assert(false); 11615 } 11616 11617 /// 11618 void setStepIncrement(int increment) { 11619 version(win32_widgets) 11620 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 11621 else version(custom_widgets) 11622 step = increment; 11623 else static assert(false); 11624 } 11625 11626 /// 11627 void addToPosition(int amount) { 11628 version(win32_widgets) 11629 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 11630 else version(custom_widgets) 11631 setPosition(current + amount); 11632 else static assert(false); 11633 } 11634 11635 /// 11636 void setPosition(int pos) { 11637 version(win32_widgets) 11638 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 11639 else version(custom_widgets) { 11640 current = pos; 11641 if(current > max) 11642 current = max; 11643 redraw(); 11644 } 11645 else static assert(false); 11646 } 11647 11648 /// 11649 void setRange(ushort min, ushort max) { 11650 version(win32_widgets) 11651 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 11652 else version(custom_widgets) { 11653 this.max = max; 11654 } 11655 else static assert(false); 11656 } 11657 11658 override int minHeight() { return 10; } 11659 } 11660 11661 version(custom_widgets) 11662 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 11663 thisLabel.reserve(label.length); 11664 bool justSawAmpersand; 11665 foreach(ch; label) { 11666 if(justSawAmpersand) { 11667 justSawAmpersand = false; 11668 if(ch == '&') { 11669 goto plain; 11670 } 11671 thisAccelerator = ch; 11672 } else { 11673 if(ch == '&') { 11674 justSawAmpersand = true; 11675 continue; 11676 } 11677 plain: 11678 thisLabel ~= ch; 11679 } 11680 } 11681 } 11682 11683 /++ 11684 Creates the fieldset (also known as a group box) with the given label. A fieldset is generally used a container for mutually exclusive [Radiobox]s. 11685 11686 11687 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 11688 11689 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11690 11691 History: 11692 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 11693 +/ 11694 class Fieldset : Widget { 11695 // FIXME: on Windows,it doesn't draw the background on the label 11696 // on X, it doesn't fix the clipping rectangle for it 11697 version(win32_widgets) 11698 override int paddingTop() { return defaultLineHeight; } 11699 else version(custom_widgets) 11700 override int paddingTop() { return defaultLineHeight + 2; } 11701 else static assert(false); 11702 override int paddingBottom() { return 6; } 11703 override int paddingLeft() { return 6; } 11704 override int paddingRight() { return 6; } 11705 11706 override int marginLeft() { return 6; } 11707 override int marginRight() { return 6; } 11708 override int marginTop() { return 2; } 11709 override int marginBottom() { return 2; } 11710 11711 string legend; 11712 11713 version(custom_widgets) private dchar accelerator; 11714 11715 this(string legend, Widget parent) { 11716 version(win32_widgets) { 11717 super(parent); 11718 this.legend = legend; 11719 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 11720 tabStop = false; 11721 } else version(custom_widgets) { 11722 super(parent); 11723 tabStop = false; 11724 11725 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 11726 } else static assert(0); 11727 } 11728 11729 version(custom_widgets) 11730 override void paint(WidgetPainter painter) { 11731 auto dlh = defaultLineHeight; 11732 11733 painter.fillColor = Color.transparent; 11734 auto cs = getComputedStyle(); 11735 painter.pen = Pen(cs.foregroundColor, 1); 11736 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 11737 11738 auto tx = painter.textSize(legend); 11739 painter.outlineColor = Color.transparent; 11740 11741 version(Windows) { 11742 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 11743 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 11744 SelectObject(painter.impl.hdc, b); 11745 } else static if(UsingSimpledisplayX11) { 11746 painter.fillColor = getComputedStyle().windowBackgroundColor; 11747 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 11748 } 11749 painter.outlineColor = cs.foregroundColor; 11750 painter.drawText(Point(8, 0), legend); 11751 } 11752 11753 override int maxHeight() { 11754 auto m = paddingTop() + paddingBottom(); 11755 foreach(child; children) { 11756 auto mh = child.maxHeight(); 11757 if(mh == int.max) 11758 return int.max; 11759 m += mh; 11760 m += child.marginBottom(); 11761 m += child.marginTop(); 11762 } 11763 m += 6; 11764 if(m < minHeight) 11765 return minHeight; 11766 return m; 11767 } 11768 11769 override int minHeight() { 11770 auto m = paddingTop() + paddingBottom(); 11771 foreach(child; children) { 11772 m += child.minHeight(); 11773 m += child.marginBottom(); 11774 m += child.marginTop(); 11775 } 11776 return m + 6; 11777 } 11778 11779 override int minWidth() { 11780 return 6 + cast(int) this.legend.length * 7; 11781 } 11782 } 11783 11784 /++ 11785 $(IMG //arsdnet.net/minigui-screenshots/windows/Fieldset.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 11786 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 11787 +/ 11788 version(minigui_screenshots) 11789 @Screenshot("Fieldset") 11790 unittest { 11791 auto window = new Window(200, 100); 11792 auto set = new Fieldset("Baby will", window); 11793 auto option1 = new Radiobox("Eat", set); 11794 auto option2 = new Radiobox("Cry", set); 11795 auto option3 = new Radiobox("Sleep", set); 11796 window.loop(); 11797 } 11798 11799 /// Draws a line 11800 class HorizontalRule : Widget { 11801 mixin Margin!q{ 2 }; 11802 override int minHeight() { return 2; } 11803 override int maxHeight() { return 2; } 11804 11805 /// 11806 this(Widget parent) { 11807 super(parent); 11808 } 11809 11810 override void paint(WidgetPainter painter) { 11811 auto cs = getComputedStyle(); 11812 painter.outlineColor = cs.darkAccentColor; 11813 painter.drawLine(Point(0, 0), Point(width, 0)); 11814 painter.outlineColor = cs.lightAccentColor; 11815 painter.drawLine(Point(0, 1), Point(width, 1)); 11816 } 11817 } 11818 11819 version(minigui_screenshots) 11820 @Screenshot("HorizontalRule") 11821 /++ 11822 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 11823 11824 +/ 11825 unittest { 11826 auto window = new Window(200, 100); 11827 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 11828 new HorizontalRule(window); 11829 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 11830 window.loop(); 11831 } 11832 11833 /// ditto 11834 class VerticalRule : Widget { 11835 mixin Margin!q{ 2 }; 11836 override int minWidth() { return 2; } 11837 override int maxWidth() { return 2; } 11838 11839 /// 11840 this(Widget parent) { 11841 super(parent); 11842 } 11843 11844 override void paint(WidgetPainter painter) { 11845 auto cs = getComputedStyle(); 11846 painter.outlineColor = cs.darkAccentColor; 11847 painter.drawLine(Point(0, 0), Point(0, height)); 11848 painter.outlineColor = cs.lightAccentColor; 11849 painter.drawLine(Point(1, 0), Point(1, height)); 11850 } 11851 } 11852 11853 11854 /// 11855 class Menu : Window { 11856 void remove() { 11857 foreach(i, child; parentWindow.children) 11858 if(child is this) { 11859 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 11860 break; 11861 } 11862 parentWindow.redraw(); 11863 11864 parentWindow.releaseMouseCapture(); 11865 } 11866 11867 /// 11868 void addSeparator() { 11869 version(win32_widgets) 11870 AppendMenu(handle, MF_SEPARATOR, 0, null); 11871 else version(custom_widgets) 11872 auto hr = new HorizontalRule(this); 11873 else static assert(0); 11874 } 11875 11876 override int paddingTop() { return 4; } 11877 override int paddingBottom() { return 4; } 11878 override int paddingLeft() { return 2; } 11879 override int paddingRight() { return 2; } 11880 11881 version(win32_widgets) {} 11882 else version(custom_widgets) { 11883 11884 Widget previouslyFocusedWidget; 11885 Widget* previouslyFocusedWidgetBelongsIn; 11886 11887 SimpleWindow dropDown; 11888 Widget menuParent; 11889 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 11890 this.menuParent = parent; 11891 11892 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 11893 previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget; 11894 parent.parentWindow.focusedWidget = this; 11895 11896 int w = 150; 11897 int h = paddingTop + paddingBottom; 11898 if(this.children.length) { 11899 // hacking it to get the ideal height out of recomputeChildLayout 11900 this.width = w; 11901 this.height = h; 11902 this.recomputeChildLayoutEntry(); 11903 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 11904 h += paddingBottom; 11905 11906 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 11907 } 11908 11909 if(offsetY == int.min) 11910 offsetY = parent.defaultLineHeight; 11911 11912 auto coord = parent.globalCoordinates(); 11913 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 11914 this.x = 0; 11915 this.y = 0; 11916 this.width = dropDown.width; 11917 this.height = dropDown.height; 11918 this.drawableWindow = dropDown; 11919 this.recomputeChildLayoutEntry(); 11920 11921 static if(UsingSimpledisplayX11) 11922 XSync(XDisplayConnection.get, 0); 11923 11924 dropDown.visibilityChanged = (bool visible) { 11925 if(visible) { 11926 this.redraw(); 11927 dropDown.grabInput(); 11928 } else { 11929 dropDown.releaseInputGrab(); 11930 } 11931 }; 11932 11933 dropDown.show(); 11934 11935 clickListener = this.addEventListener((scope ClickEvent ev) { 11936 unpopup(); 11937 // need to unlock asap just in case other user handlers block... 11938 static if(UsingSimpledisplayX11) 11939 flushGui(); 11940 }, true /* again for asap action */); 11941 } 11942 11943 EventListener clickListener; 11944 } 11945 else static assert(false); 11946 11947 version(custom_widgets) 11948 void unpopup() { 11949 mouseLastOver = mouseLastDownOn = null; 11950 dropDown.hide(); 11951 if(!menuParent.parentWindow.win.closed) { 11952 if(auto maw = cast(MouseActivatedWidget) menuParent) { 11953 maw.setDynamicState(DynamicState.depressed, false); 11954 maw.setDynamicState(DynamicState.hover, false); 11955 maw.redraw(); 11956 } 11957 // menuParent.parentWindow.win.focus(); 11958 } 11959 clickListener.disconnect(); 11960 11961 if(previouslyFocusedWidgetBelongsIn) 11962 *previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget; 11963 } 11964 11965 MenuItem[] items; 11966 11967 /// 11968 MenuItem addItem(MenuItem item) { 11969 addChild(item); 11970 items ~= item; 11971 version(win32_widgets) { 11972 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11973 } 11974 return item; 11975 } 11976 11977 string label; 11978 11979 version(win32_widgets) { 11980 HMENU handle; 11981 /// 11982 this(string label, Widget parent) { 11983 // not actually passing the parent since it effs up the drawing 11984 super(cast(Widget) null);// parent); 11985 this.label = label; 11986 handle = CreatePopupMenu(); 11987 } 11988 } else version(custom_widgets) { 11989 /// 11990 this(string label, Widget parent) { 11991 11992 if(dropDown) { 11993 dropDown.close(); 11994 } 11995 dropDown = new SimpleWindow( 11996 150, 4, 11997 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11998 11999 this.label = label; 12000 12001 super(dropDown); 12002 } 12003 } else static assert(false); 12004 12005 override int maxHeight() { return defaultLineHeight; } 12006 override int minHeight() { return defaultLineHeight; } 12007 12008 version(custom_widgets) { 12009 Widget currentPlace; 12010 12011 void changeCurrentPlace(Widget n) { 12012 if(currentPlace) { 12013 currentPlace.dynamicState = 0; 12014 } 12015 12016 if(n) { 12017 n.dynamicState = DynamicState.hover; 12018 } 12019 12020 currentPlace = n; 12021 } 12022 12023 override void paint(WidgetPainter painter) { 12024 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 12025 } 12026 12027 override void defaultEventHandler_keydown(KeyDownEvent ke) { 12028 switch(ke.key) { 12029 case Key.Down: 12030 Widget next; 12031 Widget first; 12032 foreach(w; this.children) { 12033 if((cast(MenuItem) w) is null) 12034 continue; 12035 12036 if(first is null) 12037 first = w; 12038 12039 if(next !is null) { 12040 next = w; 12041 break; 12042 } 12043 12044 if(currentPlace is null) { 12045 next = w; 12046 break; 12047 } 12048 12049 if(w is currentPlace) { 12050 next = w; 12051 } 12052 } 12053 12054 if(next is currentPlace) 12055 next = first; 12056 12057 changeCurrentPlace(next); 12058 break; 12059 case Key.Up: 12060 Widget prev; 12061 foreach(w; this.children) { 12062 if((cast(MenuItem) w) is null) 12063 continue; 12064 if(w is currentPlace) { 12065 if(prev is null) { 12066 foreach_reverse(c; this.children) { 12067 if((cast(MenuItem) c) !is null) { 12068 prev = c; 12069 break; 12070 } 12071 } 12072 } 12073 break; 12074 } 12075 prev = w; 12076 } 12077 changeCurrentPlace(prev); 12078 break; 12079 case Key.Left: 12080 case Key.Right: 12081 if(menuParent) { 12082 Menu first; 12083 Menu last; 12084 Menu prev; 12085 Menu next; 12086 bool found; 12087 12088 size_t prev_idx; 12089 size_t next_idx; 12090 12091 MenuBar mb = cast(MenuBar) menuParent.parent; 12092 12093 if(mb) { 12094 foreach(idx, menu; mb.subMenus) { 12095 if(first is null) 12096 first = menu; 12097 last = menu; 12098 if(found && next is null) { 12099 next = menu; 12100 next_idx = idx; 12101 } 12102 if(menu is this) 12103 found = true; 12104 if(!found) { 12105 prev = menu; 12106 prev_idx = idx; 12107 } 12108 } 12109 12110 Menu nextMenu; 12111 size_t nextMenuIdx; 12112 if(ke.key == Key.Left) { 12113 nextMenu = prev ? prev : last; 12114 nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1; 12115 } else { 12116 nextMenu = next ? next : first; 12117 nextMenuIdx = next ? next_idx : 0; 12118 } 12119 12120 unpopup(); 12121 12122 auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right 12123 rent.dynamicState = DynamicState.depressed | DynamicState.hover; 12124 nextMenu.popup(rent); 12125 } 12126 } 12127 break; 12128 case Key.Enter: 12129 case Key.PadEnter: 12130 // because the key up and char events will go back to the other window after we unpopup! 12131 // we will wait for the char event to come (in the following method) 12132 break; 12133 case Key.Escape: 12134 unpopup(); 12135 break; 12136 default: 12137 } 12138 } 12139 override void defaultEventHandler_char(CharEvent ke) { 12140 // if one is selected, enter activates it 12141 if(currentPlace) { 12142 if(ke.character == '\n') { 12143 // enter selects 12144 auto event = new Event(EventType.triggered, currentPlace); 12145 event.dispatch(); 12146 unpopup(); 12147 return; 12148 } 12149 } 12150 12151 // otherwise search for a hotkey 12152 foreach(item; items) { 12153 if(item.hotkey == ke.character) { 12154 auto event = new Event(EventType.triggered, item); 12155 event.dispatch(); 12156 unpopup(); 12157 return; 12158 } 12159 } 12160 } 12161 override void defaultEventHandler_mouseover(MouseOverEvent moe) { 12162 if(moe.target && moe.target.parent is this) 12163 changeCurrentPlace(moe.target); 12164 } 12165 } 12166 } 12167 12168 /++ 12169 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 12170 +/ 12171 class MenuItem : MouseActivatedWidget { 12172 Menu submenu; 12173 12174 Action action; 12175 string label; 12176 dchar hotkey; 12177 12178 override int paddingLeft() { return 4; } 12179 12180 override int maxHeight() { return defaultLineHeight + 4; } 12181 override int minHeight() { return defaultLineHeight + 4; } 12182 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 12183 override int maxWidth() { 12184 if(cast(MenuBar) parent) { 12185 return minWidth(); 12186 } 12187 return int.max; 12188 } 12189 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 12190 this(string lbl, Widget parent = null) { 12191 super(parent); 12192 //label = lbl; // FIXME 12193 foreach(idx, char ch; lbl) // FIXME 12194 if(ch != '&') { // FIXME 12195 label ~= ch; // FIXME 12196 } else { 12197 if(idx + 1 < lbl.length) { 12198 hotkey = lbl[idx + 1]; 12199 if(hotkey >= 'A' && hotkey <= 'Z') 12200 hotkey += 32; 12201 } 12202 } 12203 tabStop = false; // these are selected some other way 12204 } 12205 12206 /// 12207 this(Action action, Widget parent = null) { 12208 assert(action !is null); 12209 this(action.label, parent); 12210 this.action = action; 12211 tabStop = false; // these are selected some other way 12212 } 12213 12214 version(custom_widgets) 12215 override void paint(WidgetPainter painter) { 12216 auto cs = getComputedStyle(); 12217 if(dynamicState & DynamicState.depressed) 12218 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12219 else { 12220 if(dynamicState & DynamicState.hover) { 12221 painter.fillColor = cs.hoveringColor; 12222 painter.outlineColor = Color.transparent; 12223 } else { 12224 painter.fillColor = cs.background.color; 12225 painter.outlineColor = Color.transparent; 12226 } 12227 12228 painter.drawRectangle(Point(0, 0), Size(this.width, this.height)); 12229 } 12230 12231 if(dynamicState & DynamicState.hover) 12232 painter.outlineColor = cs.activeMenuItemColor; 12233 else 12234 painter.outlineColor = cs.foregroundColor; 12235 painter.fillColor = Color.transparent; 12236 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 12237 if(action && action.accelerator !is KeyEvent.init) { 12238 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 12239 12240 } 12241 } 12242 12243 static class Style : Widget.Style { 12244 override bool variesWithState(ulong dynamicStateFlags) { 12245 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 12246 } 12247 } 12248 mixin OverrideStyle!Style; 12249 12250 override void defaultEventHandler_triggered(Event event) { 12251 if(action) 12252 foreach(handler; action.triggered) 12253 handler(); 12254 12255 if(auto pmenu = cast(Menu) this.parent) 12256 pmenu.remove(); 12257 12258 super.defaultEventHandler_triggered(event); 12259 } 12260 } 12261 12262 version(win32_widgets) 12263 /// A "mouse activiated widget" is really just an abstract variant of button. 12264 class MouseActivatedWidget : Widget { 12265 @property bool isChecked() { 12266 assert(hwnd); 12267 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 12268 12269 } 12270 @property void isChecked(bool state) { 12271 assert(hwnd); 12272 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 12273 12274 } 12275 12276 override void handleWmCommand(ushort cmd, ushort id) { 12277 if(cmd == 0) { 12278 auto event = new Event(EventType.triggered, this); 12279 event.dispatch(); 12280 } 12281 } 12282 12283 this(Widget parent) { 12284 super(parent); 12285 } 12286 } 12287 else version(custom_widgets) 12288 /// ditto 12289 class MouseActivatedWidget : Widget { 12290 @property bool isChecked() { return isChecked_; } 12291 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 12292 12293 private bool isChecked_; 12294 12295 this(Widget parent) { 12296 super(parent); 12297 12298 addEventListener((MouseDownEvent ev) { 12299 if(ev.button == MouseButton.left) { 12300 setDynamicState(DynamicState.depressed, true); 12301 setDynamicState(DynamicState.hover, true); 12302 redraw(); 12303 } 12304 }); 12305 12306 addEventListener((MouseUpEvent ev) { 12307 if(ev.button == MouseButton.left) { 12308 setDynamicState(DynamicState.depressed, false); 12309 setDynamicState(DynamicState.hover, false); 12310 redraw(); 12311 } 12312 }); 12313 12314 addEventListener((MouseMoveEvent mme) { 12315 if(!(mme.state & ModifierState.leftButtonDown)) { 12316 if(dynamicState_ & DynamicState.depressed) { 12317 setDynamicState(DynamicState.depressed, false); 12318 redraw(); 12319 } 12320 } 12321 }); 12322 } 12323 12324 override void defaultEventHandler_focus(Event ev) { 12325 super.defaultEventHandler_focus(ev); 12326 this.redraw(); 12327 } 12328 override void defaultEventHandler_blur(Event ev) { 12329 super.defaultEventHandler_blur(ev); 12330 setDynamicState(DynamicState.depressed, false); 12331 this.redraw(); 12332 } 12333 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12334 super.defaultEventHandler_keydown(ev); 12335 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 12336 setDynamicState(DynamicState.depressed, true); 12337 setDynamicState(DynamicState.hover, true); 12338 this.redraw(); 12339 } 12340 } 12341 override void defaultEventHandler_keyup(KeyUpEvent ev) { 12342 super.defaultEventHandler_keyup(ev); 12343 if(!(dynamicState & DynamicState.depressed)) 12344 return; 12345 setDynamicState(DynamicState.depressed, false); 12346 setDynamicState(DynamicState.hover, false); 12347 this.redraw(); 12348 12349 auto event = new Event(EventType.triggered, this); 12350 event.sendDirectly(); 12351 } 12352 override void defaultEventHandler_click(ClickEvent ev) { 12353 super.defaultEventHandler_click(ev); 12354 if(ev.button == MouseButton.left) { 12355 auto event = new Event(EventType.triggered, this); 12356 event.sendDirectly(); 12357 } 12358 } 12359 12360 } 12361 else static assert(false); 12362 12363 /* 12364 /++ 12365 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 12366 12367 Basically the same as a checkbox. 12368 +/ 12369 class OnOffSwitch : MouseActivatedWidget { 12370 12371 } 12372 */ 12373 12374 /++ 12375 History: 12376 Added June 15, 2021 (dub v10.1) 12377 +/ 12378 struct ImageLabel { 12379 /++ 12380 Defines a label+image combo used by some widgets. 12381 12382 If you provide just a text label, that is all the widget will try to 12383 display. Or just an image will display just that. If you provide both, 12384 it may display both text and image side by side or display the image 12385 and offer text on an input event depending on the widget. 12386 12387 History: 12388 The `alignment` parameter was added on September 27, 2021 12389 +/ 12390 this(string label, TextAlignment alignment = TextAlignment.Center) { 12391 this.label = label; 12392 this.displayFlags = DisplayFlags.displayText; 12393 this.alignment = alignment; 12394 } 12395 12396 /// ditto 12397 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 12398 this.label = label; 12399 this.image = image; 12400 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 12401 this.alignment = alignment; 12402 } 12403 12404 /// ditto 12405 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 12406 this.image = image; 12407 this.displayFlags = DisplayFlags.displayImage; 12408 this.alignment = alignment; 12409 } 12410 12411 /// ditto 12412 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 12413 this.label = label; 12414 this.image = image; 12415 this.alignment = alignment; 12416 this.displayFlags = displayFlags; 12417 } 12418 12419 string label; 12420 MemoryImage image; 12421 12422 enum DisplayFlags { 12423 displayText = 1 << 0, 12424 displayImage = 1 << 1, 12425 } 12426 12427 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 12428 12429 TextAlignment alignment; 12430 } 12431 12432 /++ 12433 A basic checked or not checked box with an attached label. 12434 12435 12436 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12437 12438 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12439 12440 History: 12441 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12442 +/ 12443 class Checkbox : MouseActivatedWidget { 12444 version(win32_widgets) { 12445 override int maxHeight() { return scaleWithDpi(16); } 12446 override int minHeight() { return scaleWithDpi(16); } 12447 } else version(custom_widgets) { 12448 private enum buttonSize = 16; 12449 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 12450 override int minHeight() { return maxHeight(); } 12451 } else static assert(0); 12452 12453 override int marginLeft() { return 4; } 12454 12455 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 12456 12457 /++ 12458 Just an alias because I keep typing checked out of web habit. 12459 12460 History: 12461 Added May 31, 2021 12462 +/ 12463 alias checked = isChecked; 12464 12465 private string label; 12466 private dchar accelerator; 12467 12468 /++ 12469 +/ 12470 this(string label, Widget parent) { 12471 this(ImageLabel(label), Appearance.checkbox, parent); 12472 } 12473 12474 /// ditto 12475 this(string label, Appearance appearance, Widget parent) { 12476 this(ImageLabel(label), appearance, parent); 12477 } 12478 12479 /++ 12480 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 12481 12482 History: 12483 Added June 29, 2021 (dub v10.2) 12484 +/ 12485 enum Appearance { 12486 checkbox, /// a normal checkbox 12487 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 12488 //sliderswitch, 12489 } 12490 private Appearance appearance; 12491 12492 /// ditto 12493 private this(ImageLabel label, Appearance appearance, Widget parent) { 12494 super(parent); 12495 version(win32_widgets) { 12496 this.label = label.label; 12497 12498 uint extraStyle; 12499 final switch(appearance) { 12500 case Appearance.checkbox: 12501 break; 12502 case Appearance.pushbutton: 12503 extraStyle |= BS_PUSHLIKE; 12504 break; 12505 } 12506 12507 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 12508 } else version(custom_widgets) { 12509 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 12510 } else static assert(0); 12511 } 12512 12513 version(custom_widgets) 12514 override void paint(WidgetPainter painter) { 12515 auto cs = getComputedStyle(); 12516 if(isFocused()) { 12517 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 12518 painter.fillColor = cs.windowBackgroundColor; 12519 painter.drawRectangle(Point(0, 0), width, height); 12520 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 12521 } else { 12522 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 12523 painter.fillColor = cs.windowBackgroundColor; 12524 painter.drawRectangle(Point(0, 0), width, height); 12525 } 12526 12527 12528 painter.outlineColor = Color.black; 12529 painter.fillColor = Color.white; 12530 enum rectOffset = 2; 12531 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 12532 12533 if(isChecked) { 12534 auto size = scaleWithDpi(2); 12535 painter.pen = Pen(Color.black, size); 12536 // I'm using height so the checkbox is square 12537 enum padding = 3; 12538 painter.drawLine( 12539 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 12540 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 12541 ); 12542 painter.drawLine( 12543 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 12544 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 12545 ); 12546 12547 painter.pen = Pen(Color.black, 1); 12548 } 12549 12550 if(label !is null) { 12551 painter.outlineColor = cs.foregroundColor(); 12552 painter.fillColor = cs.foregroundColor(); 12553 12554 // i want the centerline of the text to be aligned with the centerline of the checkbox 12555 /+ 12556 auto font = cs.font(); 12557 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 12558 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 12559 +/ 12560 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 12561 } 12562 } 12563 12564 override void defaultEventHandler_triggered(Event ev) { 12565 isChecked = !isChecked; 12566 12567 this.emit!(ChangeEvent!bool)(&isChecked); 12568 12569 redraw(); 12570 } 12571 12572 /// Emits a change event with the checked state 12573 mixin Emits!(ChangeEvent!bool); 12574 } 12575 12576 /// Adds empty space to a layout. 12577 class VerticalSpacer : Widget { 12578 private int mh; 12579 12580 /++ 12581 History: 12582 The overload with `maxHeight` was added on December 31, 2024 12583 +/ 12584 this(Widget parent) { 12585 this(0, parent); 12586 } 12587 12588 /// ditto 12589 this(int maxHeight, Widget parent) { 12590 this.mh = maxHeight; 12591 super(parent); 12592 this.tabStop = false; 12593 } 12594 12595 override int maxHeight() { 12596 return mh ? scaleWithDpi(mh) : super.maxHeight(); 12597 } 12598 } 12599 12600 12601 /// ditto 12602 class HorizontalSpacer : Widget { 12603 private int mw; 12604 12605 /++ 12606 History: 12607 The overload with `maxWidth` was added on December 31, 2024 12608 +/ 12609 this(Widget parent) { 12610 this(0, parent); 12611 } 12612 12613 /// ditto 12614 this(int maxWidth, Widget parent) { 12615 this.mw = maxWidth; 12616 super(parent); 12617 this.tabStop = false; 12618 } 12619 12620 override int maxWidth() { 12621 return mw ? scaleWithDpi(mw) : super.maxWidth(); 12622 } 12623 } 12624 12625 12626 /++ 12627 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 12628 12629 12630 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12631 12632 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12633 12634 History: 12635 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12636 +/ 12637 class Radiobox : MouseActivatedWidget { 12638 12639 version(win32_widgets) { 12640 override int maxHeight() { return scaleWithDpi(16); } 12641 override int minHeight() { return scaleWithDpi(16); } 12642 } else version(custom_widgets) { 12643 private enum buttonSize = 16; 12644 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 12645 override int minHeight() { return maxHeight(); } 12646 } else static assert(0); 12647 12648 override int marginLeft() { return 4; } 12649 12650 // FIXME: make a label getter 12651 private string label; 12652 private dchar accelerator; 12653 12654 /++ 12655 12656 +/ 12657 this(string label, Widget parent) { 12658 super(parent); 12659 version(win32_widgets) { 12660 this.label = label; 12661 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 12662 } else version(custom_widgets) { 12663 label.extractWindowsStyleLabel(this.label, this.accelerator); 12664 height = 16; 12665 width = height + 4 + cast(int) label.length * 16; 12666 } 12667 } 12668 12669 version(custom_widgets) 12670 override void paint(WidgetPainter painter) { 12671 auto cs = getComputedStyle(); 12672 12673 if(isFocused) { 12674 painter.fillColor = cs.windowBackgroundColor; 12675 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 12676 } else { 12677 painter.fillColor = cs.windowBackgroundColor; 12678 painter.outlineColor = cs.windowBackgroundColor; 12679 } 12680 painter.drawRectangle(Point(0, 0), width, height); 12681 12682 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 12683 12684 painter.outlineColor = Color.black; 12685 painter.fillColor = Color.white; 12686 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 12687 if(isChecked) { 12688 painter.outlineColor = Color.black; 12689 painter.fillColor = Color.black; 12690 // I'm using height so the checkbox is square 12691 auto size = scaleWithDpi(2); 12692 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 12693 } 12694 12695 painter.outlineColor = cs.foregroundColor(); 12696 painter.fillColor = cs.foregroundColor(); 12697 12698 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 12699 } 12700 12701 12702 override void defaultEventHandler_triggered(Event ev) { 12703 isChecked = true; 12704 12705 if(this.parent) { 12706 foreach(child; this.parent.children) { 12707 if(child is this) continue; 12708 if(auto rb = cast(Radiobox) child) { 12709 rb.isChecked = false; 12710 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 12711 rb.redraw(); 12712 } 12713 } 12714 } 12715 12716 this.emit!(ChangeEvent!bool)(&this.isChecked); 12717 12718 redraw(); 12719 } 12720 12721 /// 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. 12722 mixin Emits!(ChangeEvent!bool); 12723 } 12724 12725 12726 /++ 12727 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 12728 12729 12730 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12731 12732 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12733 12734 History: 12735 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12736 +/ 12737 class Button : MouseActivatedWidget { 12738 override int heightStretchiness() { return 3; } 12739 override int widthStretchiness() { return 3; } 12740 12741 /++ 12742 If true, this button will emit trigger events on double (and other quick events, if added) click events as well as on normal single click events. 12743 12744 History: 12745 Added July 2, 2021 12746 +/ 12747 public bool triggersOnMultiClick; 12748 12749 private string label_; 12750 private TextAlignment alignment; 12751 private dchar accelerator; 12752 12753 /// 12754 string label() { return label_; } 12755 /// 12756 void label(string l) { 12757 label_ = l; 12758 version(win32_widgets) { 12759 WCharzBuffer bfr = WCharzBuffer(l); 12760 SetWindowTextW(hwnd, bfr.ptr); 12761 } else version(custom_widgets) { 12762 redraw(); 12763 } 12764 } 12765 12766 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 12767 super.defaultEventHandler_dblclick(ev); 12768 if(triggersOnMultiClick) { 12769 if(ev.button == MouseButton.left) { 12770 auto event = new Event(EventType.triggered, this); 12771 event.sendDirectly(); 12772 } 12773 } 12774 } 12775 12776 private Sprite sprite; 12777 private int displayFlags; 12778 12779 /++ 12780 Creates a push button with the given label, which may be an image or some text. 12781 12782 Bugs: 12783 If the image is bigger than the button, it may not be displayed in the right position on Linux. 12784 12785 History: 12786 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 12787 12788 The button with label and image will respect requests to show both on Windows as 12789 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 12790 +/ 12791 this(ImageLabel label, Widget parent) { 12792 version(win32_widgets) { 12793 // FIXME: use ideal button size instead 12794 width = 50; 12795 height = 30; 12796 super(parent); 12797 12798 // BS_BITMAP is set when we want image only, so checking for exactly that combination 12799 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 12800 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 12801 12802 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 12803 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 12804 12805 if(label.image) { 12806 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 12807 12808 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 12809 } 12810 12811 this.label = label.label; 12812 } else version(custom_widgets) { 12813 width = 50; 12814 height = 30; 12815 super(parent); 12816 12817 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 12818 12819 if(label.image) { 12820 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 12821 this.displayFlags = label.displayFlags; 12822 } 12823 12824 this.alignment = label.alignment; 12825 } 12826 } 12827 12828 /// 12829 this(string label, Widget parent) { 12830 this(ImageLabel(label), parent); 12831 } 12832 12833 override int minHeight() { return defaultLineHeight + 4; } 12834 12835 static class Style : Widget.Style { 12836 override WidgetBackground background() { 12837 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 12838 12839 auto pressed = DynamicState.depressed | DynamicState.hover; 12840 if((widget.dynamicState & pressed) == pressed) { 12841 return WidgetBackground(cs.depressedButtonColor()); 12842 } else if(widget.dynamicState & DynamicState.hover) { 12843 return WidgetBackground(cs.hoveringColor()); 12844 } else { 12845 return WidgetBackground(cs.buttonColor()); 12846 } 12847 } 12848 12849 override FrameStyle borderStyle() { 12850 auto pressed = DynamicState.depressed | DynamicState.hover; 12851 if((widget.dynamicState & pressed) == pressed) { 12852 return FrameStyle.sunk; 12853 } else { 12854 return FrameStyle.risen; 12855 } 12856 12857 } 12858 12859 override bool variesWithState(ulong dynamicStateFlags) { 12860 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 12861 } 12862 } 12863 mixin OverrideStyle!Style; 12864 12865 version(custom_widgets) 12866 override void paint(WidgetPainter painter) { 12867 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 12868 if(sprite) { 12869 sprite.drawAt( 12870 painter, 12871 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 12872 Point(0, 0) 12873 ); 12874 } else { 12875 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 12876 } 12877 return bounds; 12878 }); 12879 } 12880 12881 override int flexBasisWidth() { 12882 version(win32_widgets) { 12883 SIZE size; 12884 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 12885 if(size.cx == 0) 12886 goto fallback; 12887 return size.cx + scaleWithDpi(16); 12888 } 12889 fallback: 12890 return scaleWithDpi(cast(int) label.length * 8 + 16); 12891 } 12892 12893 override int flexBasisHeight() { 12894 version(win32_widgets) { 12895 SIZE size; 12896 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 12897 if(size.cy == 0) 12898 goto fallback; 12899 return size.cy + scaleWithDpi(6); 12900 } 12901 fallback: 12902 return defaultLineHeight + 4; 12903 } 12904 } 12905 12906 /++ 12907 A button with a consistent size, suitable for user commands like OK and CANCEL. 12908 +/ 12909 class CommandButton : Button { 12910 this(string label, Widget parent) { 12911 super(label, parent); 12912 } 12913 12914 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 12915 12916 override int maxHeight() { 12917 return defaultLineHeight + 4; 12918 } 12919 12920 override int maxWidth() { 12921 return defaultLineHeight * 4; 12922 } 12923 12924 override int marginLeft() { return 12; } 12925 override int marginRight() { return 12; } 12926 override int marginTop() { return 12; } 12927 override int marginBottom() { return 12; } 12928 } 12929 12930 /// 12931 enum ArrowDirection { 12932 left, /// 12933 right, /// 12934 up, /// 12935 down /// 12936 } 12937 12938 /// 12939 version(custom_widgets) 12940 class ArrowButton : Button { 12941 /// 12942 this(ArrowDirection direction, Widget parent) { 12943 super("", parent); 12944 this.direction = direction; 12945 triggersOnMultiClick = true; 12946 } 12947 12948 private ArrowDirection direction; 12949 12950 override int minHeight() { return scaleWithDpi(16); } 12951 override int maxHeight() { return scaleWithDpi(16); } 12952 override int minWidth() { return scaleWithDpi(16); } 12953 override int maxWidth() { return scaleWithDpi(16); } 12954 12955 override void paint(WidgetPainter painter) { 12956 super.paint(painter); 12957 12958 auto cs = getComputedStyle(); 12959 12960 painter.outlineColor = cs.foregroundColor; 12961 painter.fillColor = cs.foregroundColor; 12962 12963 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 12964 12965 final switch(direction) { 12966 case ArrowDirection.up: 12967 painter.drawPolygon( 12968 scaleWithDpi(Point(2, 10) + offset), 12969 scaleWithDpi(Point(7, 5) + offset), 12970 scaleWithDpi(Point(12, 10) + offset), 12971 scaleWithDpi(Point(2, 10) + offset) 12972 ); 12973 break; 12974 case ArrowDirection.down: 12975 painter.drawPolygon( 12976 scaleWithDpi(Point(2, 6) + offset), 12977 scaleWithDpi(Point(7, 11) + offset), 12978 scaleWithDpi(Point(12, 6) + offset), 12979 scaleWithDpi(Point(2, 6) + offset) 12980 ); 12981 break; 12982 case ArrowDirection.left: 12983 painter.drawPolygon( 12984 scaleWithDpi(Point(10, 2) + offset), 12985 scaleWithDpi(Point(5, 7) + offset), 12986 scaleWithDpi(Point(10, 12) + offset), 12987 scaleWithDpi(Point(10, 2) + offset) 12988 ); 12989 break; 12990 case ArrowDirection.right: 12991 painter.drawPolygon( 12992 scaleWithDpi(Point(6, 2) + offset), 12993 scaleWithDpi(Point(11, 7) + offset), 12994 scaleWithDpi(Point(6, 12) + offset), 12995 scaleWithDpi(Point(6, 2) + offset) 12996 ); 12997 break; 12998 } 12999 } 13000 } 13001 13002 private 13003 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 13004 int x, y; 13005 Widget par = c; 13006 while(par) { 13007 x += par.x; 13008 y += par.y; 13009 par = par.parent; 13010 } 13011 return [x, y]; 13012 } 13013 13014 version(win32_widgets) 13015 private 13016 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 13017 // MapWindowPoints? 13018 int x, y; 13019 Widget par = c; 13020 while(par) { 13021 x += par.x; 13022 y += par.y; 13023 par = par.parent; 13024 if(par !is null && par.useNativeDrawing()) 13025 break; 13026 } 13027 return [x, y]; 13028 } 13029 13030 /// 13031 class ImageBox : Widget { 13032 private MemoryImage image_; 13033 13034 override int widthStretchiness() { return 1; } 13035 override int heightStretchiness() { return 1; } 13036 override int widthShrinkiness() { return 1; } 13037 override int heightShrinkiness() { return 1; } 13038 13039 override int flexBasisHeight() { 13040 return image_.height; 13041 } 13042 13043 override int flexBasisWidth() { 13044 return image_.width; 13045 } 13046 13047 /// 13048 public void setImage(MemoryImage image){ 13049 this.image_ = image; 13050 if(this.parentWindow && this.parentWindow.win) { 13051 if(sprite) 13052 sprite.dispose(); 13053 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13054 } 13055 redraw(); 13056 } 13057 13058 /// How to fit the image in the box if they aren't an exact match in size? 13059 enum HowToFit { 13060 center, /// centers the image, cropping around all the edges as needed 13061 crop, /// always draws the image in the upper left, cropping the lower right if needed 13062 // stretch, /// not implemented 13063 } 13064 13065 private Sprite sprite; 13066 private HowToFit howToFit_; 13067 13068 private Color backgroundColor_; 13069 13070 /// 13071 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 13072 this.image_ = image; 13073 this.tabStop = false; 13074 this.howToFit_ = howToFit; 13075 this.backgroundColor_ = backgroundColor; 13076 super(parent); 13077 updateSprite(); 13078 } 13079 13080 /// ditto 13081 this(MemoryImage image, HowToFit howToFit, Widget parent) { 13082 this(image, howToFit, Color.transparent, parent); 13083 } 13084 13085 private void updateSprite() { 13086 if(sprite is null && this.parentWindow && this.parentWindow.win) { 13087 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13088 } 13089 } 13090 13091 override void paint(WidgetPainter painter) { 13092 updateSprite(); 13093 if(backgroundColor_.a) { 13094 painter.fillColor = backgroundColor_; 13095 painter.drawRectangle(Point(0, 0), width, height); 13096 } 13097 if(howToFit_ == HowToFit.crop) 13098 sprite.drawAt(painter, Point(0, 0)); 13099 else if(howToFit_ == HowToFit.center) { 13100 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 13101 } 13102 } 13103 } 13104 13105 /// 13106 class TextLabel : Widget { 13107 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 13108 override int maxHeight() { return minHeight; } 13109 override int minWidth() { return 32; } 13110 13111 override int flexBasisHeight() { return minHeight(); } 13112 override int flexBasisWidth() { return defaultTextWidth(label); } 13113 13114 string label_; 13115 13116 /++ 13117 Indicates which other control this label is here for. Similar to HTML `for` attribute. 13118 13119 In practice this means a click on the label will focus the `labelFor`. In future versions 13120 it will also set screen reader hints but that is not yet implemented. 13121 13122 History: 13123 Added October 3, 2021 (dub v10.4) 13124 +/ 13125 Widget labelFor; 13126 13127 /// 13128 @scriptable 13129 string label() { return label_; } 13130 13131 /// 13132 @scriptable 13133 void label(string l) { 13134 label_ = l; 13135 version(win32_widgets) { 13136 WCharzBuffer bfr = WCharzBuffer(l); 13137 SetWindowTextW(hwnd, bfr.ptr); 13138 } else version(custom_widgets) 13139 redraw(); 13140 } 13141 13142 override void defaultEventHandler_click(scope ClickEvent ce) { 13143 if(this.labelFor !is null) 13144 this.labelFor.focus(); 13145 } 13146 13147 /++ 13148 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 13149 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 13150 +/ 13151 this(string label, TextAlignment alignment, Widget parent) { 13152 this.label_ = label; 13153 this.alignment = alignment; 13154 this.tabStop = false; 13155 super(parent); 13156 13157 version(win32_widgets) 13158 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 13159 } 13160 13161 /// ditto 13162 this(string label, Widget parent) { 13163 this(label, TextAlignment.Right, parent); 13164 } 13165 13166 TextAlignment alignment; 13167 13168 version(custom_widgets) 13169 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13170 painter.outlineColor = getComputedStyle().foregroundColor; 13171 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 13172 return bounds; 13173 } 13174 } 13175 13176 class TextDisplayHelper : Widget { 13177 protected TextLayouter l; 13178 protected ScrollMessageWidget smw; 13179 13180 private const(TextLayouter.State)*[] undoStack; 13181 private const(TextLayouter.State)*[] redoStack; 13182 13183 private string preservedPrimaryText; 13184 protected void selectionChanged() { 13185 // sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString()); 13186 static if(UsingSimpledisplayX11) 13187 with(l.selection()) { 13188 if(!isEmpty()) { 13189 //sdpyPrintDebugString("!isEmpty"); 13190 13191 getPrimarySelection(parentWindow.win, (in char[] txt) { 13192 // sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")"); 13193 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 13194 if(txt.length) { 13195 preservedPrimaryText = txt.idup; 13196 // writeln(preservedPrimaryText); 13197 } 13198 13199 setPrimarySelection(parentWindow.win, getContentString()); 13200 }); 13201 } 13202 } 13203 } 13204 13205 final TextLayouter layouter() { 13206 return l; 13207 } 13208 13209 bool readonly; 13210 bool caretNavigation; // scroll lock can flip this 13211 bool singleLine; 13212 bool acceptsTabInput; 13213 13214 private Menu ctx; 13215 override Menu contextMenu(int x, int y) { 13216 if(ctx is null) { 13217 ctx = new Menu("Actions", this); 13218 if(!readonly) { 13219 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 13220 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 13221 ctx.addSeparator(); 13222 } 13223 if(!readonly) 13224 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 13225 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 13226 if(!readonly) 13227 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 13228 if(!readonly) 13229 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 13230 ctx.addSeparator(); 13231 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 13232 } 13233 return ctx; 13234 } 13235 13236 override void defaultEventHandler_blur(Event ev) { 13237 super.defaultEventHandler_blur(ev); 13238 if(l.wasMutated()) { 13239 auto evt = new ChangeEvent!string(this, &this.content); 13240 evt.dispatch(); 13241 l.clearWasMutatedFlag(); 13242 } 13243 } 13244 13245 private string content() { 13246 return l.getTextString(); 13247 } 13248 13249 void undo() { 13250 if(readonly) return; 13251 if(undoStack.length) { 13252 auto state = undoStack[$-1]; 13253 undoStack = undoStack[0 .. $-1]; 13254 undoStack.assumeSafeAppend(); 13255 redoStack ~= l.saveState(); 13256 l.restoreState(state); 13257 adjustScrollbarSizes(); 13258 scrollForCaret(); 13259 redraw(); 13260 stateCheckpoint = true; 13261 } 13262 } 13263 13264 void redo() { 13265 if(readonly) return; 13266 if(redoStack.length) { 13267 doStateCheckpoint(); 13268 auto state = redoStack[$-1]; 13269 redoStack = redoStack[0 .. $-1]; 13270 redoStack.assumeSafeAppend(); 13271 l.restoreState(state); 13272 adjustScrollbarSizes(); 13273 scrollForCaret(); 13274 redraw(); 13275 stateCheckpoint = true; 13276 } 13277 } 13278 13279 void cut() { 13280 if(readonly) return; 13281 with(l.selection()) { 13282 if(!isEmpty()) { 13283 setClipboardText(parentWindow.win, getContentString()); 13284 doStateCheckpoint(); 13285 replaceContent(""); 13286 adjustScrollbarSizes(); 13287 scrollForCaret(); 13288 this.redraw(); 13289 } 13290 } 13291 13292 } 13293 13294 void copy() { 13295 with(l.selection()) { 13296 if(!isEmpty()) { 13297 setClipboardText(parentWindow.win, getContentString()); 13298 this.redraw(); 13299 } 13300 } 13301 } 13302 13303 void paste() { 13304 if(readonly) return; 13305 getClipboardText(parentWindow.win, (txt) { 13306 doStateCheckpoint(); 13307 if(singleLine) 13308 l.selection.replaceContent(txt.stripInternal()); 13309 else 13310 l.selection.replaceContent(txt); 13311 adjustScrollbarSizes(); 13312 scrollForCaret(); 13313 this.redraw(); 13314 }); 13315 } 13316 13317 void deleteContentOfSelection() { 13318 if(readonly) return; 13319 doStateCheckpoint(); 13320 l.selection.replaceContent(""); 13321 l.selection.setUserXCoordinate(); 13322 adjustScrollbarSizes(); 13323 scrollForCaret(); 13324 redraw(); 13325 } 13326 13327 void selectAll() { 13328 with(l.selection) { 13329 moveToStartOfDocument(); 13330 setAnchor(); 13331 moveToEndOfDocument(); 13332 setFocus(); 13333 13334 selectionChanged(); 13335 } 13336 redraw(); 13337 } 13338 13339 protected bool stateCheckpoint = true; 13340 13341 protected void doStateCheckpoint() { 13342 if(stateCheckpoint) { 13343 undoStack ~= l.saveState(); 13344 stateCheckpoint = false; 13345 } 13346 } 13347 13348 protected void adjustScrollbarSizes() { 13349 // FIXME: will want a content area helper function instead of doing all these subtractions myself 13350 auto borderWidth = 2; 13351 this.smw.setTotalArea(l.width, l.height); 13352 this.smw.setViewableArea( 13353 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 13354 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 13355 } 13356 13357 protected void scrollForCaret() { 13358 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 13359 smw.scrollIntoView(l.selection.focusBoundingBox()); 13360 } 13361 13362 // FIXME: this should be a theme changed event listener instead 13363 private BaseVisualTheme currentTheme; 13364 override void recomputeChildLayout() { 13365 if(currentTheme is null) 13366 currentTheme = WidgetPainter.visualTheme; 13367 if(WidgetPainter.visualTheme !is currentTheme) { 13368 currentTheme = WidgetPainter.visualTheme; 13369 auto ds = this.l.defaultStyle; 13370 if(auto ms = cast(MyTextStyle) ds) { 13371 auto cs = getComputedStyle(); 13372 auto font = cs.font(); 13373 if(font !is null) 13374 ms.font_ = font; 13375 else { 13376 auto osc = new OperatingSystemFont(); 13377 osc.loadDefault; 13378 ms.font_ = osc; 13379 } 13380 } 13381 } 13382 super.recomputeChildLayout(); 13383 } 13384 13385 private Point adjustForSingleLine(Point p) { 13386 if(singleLine) 13387 return Point(p.x, this.height / 2); 13388 else 13389 return p; 13390 } 13391 13392 private bool wordWrapEnabled_; 13393 13394 this(TextLayouter l, ScrollMessageWidget parent) { 13395 this.smw = parent; 13396 13397 smw.addDefaultWheelListeners(16, 16, 8); 13398 smw.movementPerButtonClick(16, 16); 13399 13400 this.defaultPadding = Rectangle(2, 2, 2, 2); 13401 13402 this.l = l; 13403 super(parent); 13404 13405 smw.addEventListener((scope ScrollEvent se) { 13406 this.redraw(); 13407 }); 13408 13409 bool mouseDown; 13410 bool mouseActuallyMoved; 13411 13412 this.addEventListener((scope ResizeEvent re) { 13413 // FIXME: I should add a method to give this client area width thing 13414 if(wordWrapEnabled_) 13415 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 13416 13417 adjustScrollbarSizes(); 13418 scrollForCaret(); 13419 13420 this.redraw(); 13421 }); 13422 13423 this.addEventListener((scope KeyDownEvent kde) { 13424 switch(kde.key) { 13425 case Key.Up, Key.Down, Key.Left, Key.Right: 13426 case Key.Home, Key.End: 13427 stateCheckpoint = true; 13428 bool setPosition = false; 13429 switch(kde.key) { 13430 case Key.Up: l.selection.moveUp(); break; 13431 case Key.Down: l.selection.moveDown(); break; 13432 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 13433 case Key.Right: l.selection.moveRight(); setPosition = true; break; 13434 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 13435 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 13436 default: assert(0); 13437 } 13438 13439 if(kde.shiftKey) 13440 l.selection.setFocus(); 13441 else 13442 l.selection.setAnchor(); 13443 13444 selectionChanged(); 13445 13446 if(setPosition) 13447 l.selection.setUserXCoordinate(); 13448 scrollForCaret(); 13449 redraw(); 13450 break; 13451 case Key.PageUp, Key.PageDown: 13452 // FIXME 13453 scrollForCaret(); 13454 break; 13455 case Key.Delete: 13456 if(l.selection.isEmpty()) { 13457 l.selection.setAnchor(); 13458 l.selection.moveRight(); 13459 l.selection.setFocus(); 13460 } 13461 deleteContentOfSelection(); 13462 adjustScrollbarSizes(); 13463 scrollForCaret(); 13464 break; 13465 case Key.Insert: 13466 break; 13467 case Key.A: 13468 if(kde.ctrlKey) 13469 selectAll(); 13470 break; 13471 case Key.F: 13472 // find 13473 break; 13474 case Key.Z: 13475 if(kde.ctrlKey) 13476 undo(); 13477 break; 13478 case Key.R: 13479 if(kde.ctrlKey) 13480 redo(); 13481 break; 13482 case Key.X: 13483 if(kde.ctrlKey) 13484 cut(); 13485 break; 13486 case Key.C: 13487 if(kde.ctrlKey) 13488 copy(); 13489 break; 13490 case Key.V: 13491 if(kde.ctrlKey) 13492 paste(); 13493 break; 13494 case Key.F1: 13495 with(l.selection()) { 13496 moveToStartOfLine(); 13497 setAnchor(); 13498 moveToEndOfLine(); 13499 moveToIncludeAdjacentEndOfLineMarker(); 13500 setFocus(); 13501 replaceContent(""); 13502 } 13503 13504 redraw(); 13505 break; 13506 /* 13507 case Key.F2: 13508 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 13509 //(cast(MyTextStyle) old).font, 13510 font2, 13511 Color.red))); 13512 redraw(); 13513 break; 13514 */ 13515 case Key.Tab: 13516 // we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl 13517 if(acceptsTabInput && !kde.ctrlKey) 13518 kde.preventDefault(); 13519 break; 13520 default: 13521 } 13522 }); 13523 13524 Point downAt; 13525 13526 static if(UsingSimpledisplayX11) 13527 this.addEventListener((scope ClickEvent ce) { 13528 if(ce.button == MouseButton.middle) { 13529 parentWindow.win.getPrimarySelection((txt) { 13530 doStateCheckpoint(); 13531 13532 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 13533 13534 if(txt == l.selection.getContentString && preservedPrimaryText.length) 13535 l.selection.replaceContent(preservedPrimaryText); 13536 else 13537 l.selection.replaceContent(txt); 13538 redraw(); 13539 }); 13540 } 13541 }); 13542 13543 this.addEventListener((scope DoubleClickEvent dce) { 13544 if(dce.button == MouseButton.left) { 13545 with(l.selection()) { 13546 scope dg = delegate const(char)[] (scope return const(char)[] ch) { 13547 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 13548 return ch; 13549 return null; 13550 }; 13551 find(dg, 1, true).moveToEnd.setAnchor; 13552 find(dg, 1, false).moveTo.setFocus; 13553 selectionChanged(); 13554 redraw(); 13555 } 13556 } 13557 }); 13558 13559 this.addEventListener((scope MouseDownEvent ce) { 13560 if(ce.button == MouseButton.left) { 13561 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 13562 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 13563 l.selection.setAnchor(); 13564 mouseDown = true; 13565 mouseActuallyMoved = false; 13566 parentWindow.captureMouse(this); 13567 this.redraw(); 13568 } 13569 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 13570 }); 13571 13572 Timer autoscrollTimer; 13573 int autoscrollDirection; 13574 int autoscrollAmount; 13575 13576 void autoscroll() { 13577 switch(autoscrollDirection) { 13578 case 0: smw.scrollUp(autoscrollAmount); break; 13579 case 1: smw.scrollDown(autoscrollAmount); break; 13580 case 2: smw.scrollLeft(autoscrollAmount); break; 13581 case 3: smw.scrollRight(autoscrollAmount); break; 13582 default: assert(0); 13583 } 13584 13585 this.redraw(); 13586 } 13587 13588 void setAutoscrollTimer(int direction, int amount) { 13589 if(autoscrollTimer is null) { 13590 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 13591 } 13592 13593 autoscrollDirection = direction; 13594 autoscrollAmount = amount; 13595 } 13596 13597 void stopAutoscrollTimer() { 13598 if(autoscrollTimer !is null) { 13599 autoscrollTimer.dispose(); 13600 autoscrollTimer = null; 13601 } 13602 autoscrollAmount = 0; 13603 autoscrollDirection = 0; 13604 } 13605 13606 this.addEventListener((scope MouseMoveEvent ce) { 13607 if(mouseDown) { 13608 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 13609 13610 // FIXME: when scrolling i actually do want a timer. 13611 // i also want a zone near the sides of the window where i can auto scroll 13612 13613 auto scrollMultiplier = scaleWithDpi(16); 13614 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 13615 13616 if(!singleLine && movedTo.y < 4) { 13617 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 13618 } else 13619 if(!singleLine && (movedTo.y + 6) > this.height) { 13620 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 13621 } else 13622 if(movedTo.x < 4) { 13623 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 13624 } else 13625 if((movedTo.x + 6) > this.width) { 13626 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 13627 } else 13628 stopAutoscrollTimer(); 13629 13630 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 13631 l.selection.setFocus(); 13632 mouseActuallyMoved = true; 13633 this.redraw(); 13634 } 13635 }); 13636 13637 this.addEventListener((scope MouseUpEvent ce) { 13638 // FIXME: assert primary selection 13639 if(mouseDown && ce.button == MouseButton.left) { 13640 stateCheckpoint = true; 13641 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 13642 //l.selection.setFocus(); 13643 mouseDown = false; 13644 parentWindow.releaseMouseCapture(); 13645 stopAutoscrollTimer(); 13646 this.redraw(); 13647 13648 if(mouseActuallyMoved) 13649 selectionChanged(); 13650 } 13651 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 13652 }); 13653 13654 this.addEventListener((scope CharEvent ce) { 13655 if(readonly) 13656 return; 13657 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 13658 return; // skip the ctrl+x characters we don't care about as plain text 13659 13660 if(singleLine && ce.character == '\n') 13661 return; 13662 if(!acceptsTabInput && ce.character == '\t') 13663 return; 13664 13665 doStateCheckpoint(); 13666 13667 char[4] buffer; 13668 import arsd.core; 13669 auto stride = encodeUtf8(buffer, ce.character); 13670 l.selection.replaceContent(buffer[0 .. stride]); 13671 l.selection.setUserXCoordinate(); 13672 adjustScrollbarSizes(); 13673 scrollForCaret(); 13674 redraw(); 13675 }); 13676 } 13677 13678 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 13679 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 13680 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent 13681 if(parent && parent.parent && parent.parent.parent) 13682 parent.parent.parent.useStyleProperties(dg); 13683 else 13684 super.useStyleProperties(dg); 13685 } 13686 13687 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 13688 override int maxHeight() { 13689 if(singleLine) 13690 return minHeight; 13691 else 13692 return super.maxHeight(); 13693 } 13694 13695 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13696 painter.drawText(upperLeft, text); 13697 } 13698 13699 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13700 //painter.setFont(font); 13701 13702 auto cs = getComputedStyle(); 13703 auto defaultColor = cs.foregroundColor; 13704 13705 auto old = painter.setClipRectangle(bounds); 13706 scope(exit) painter.setClipRectangle(old); 13707 13708 l.getDrawableText(delegate bool(txt, style, info, carets...) { 13709 //writeln("Segment: ", txt); 13710 assert(style !is null); 13711 13712 auto myStyle = cast(MyTextStyle) style; 13713 assert(myStyle !is null); 13714 13715 painter.setFont(myStyle.font); 13716 // defaultColor = myStyle.color; // FIXME: so wrong 13717 13718 if(info.selections && info.boundingBox.width > 0) { 13719 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 13720 painter.fillColor = color; 13721 painter.outlineColor = color; 13722 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 13723 painter.outlineColor = cs.selectionForegroundColor; 13724 //painter.fillColor = Color.white; 13725 } else { 13726 painter.outlineColor = defaultColor; 13727 } 13728 13729 if(this.isFocused) 13730 foreach(idx, caret; carets) { 13731 if(idx == 0) 13732 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 13733 painter.drawLine( 13734 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 13735 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 13736 ); 13737 } 13738 13739 if(txt.stripInternal.length) { 13740 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 13741 } 13742 13743 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 13744 return false; 13745 } else { 13746 return true; 13747 } 13748 }, Rectangle(smw.position(), bounds.size)); 13749 13750 /+ 13751 int place = 0; 13752 int y = 75; 13753 foreach(width; widths) { 13754 painter.fillColor = Color.red; 13755 painter.drawRectangle(Point(place, y), Size(width, 75)); 13756 //y += 15; 13757 place += width; 13758 } 13759 +/ 13760 13761 return bounds; 13762 } 13763 13764 static class MyTextStyle : TextStyle { 13765 OperatingSystemFont font_; 13766 this(OperatingSystemFont font, bool passwordMode = false) { 13767 this.font_ = font; 13768 } 13769 13770 override OperatingSystemFont font() { 13771 return font_; 13772 } 13773 } 13774 } 13775 13776 /+ 13777 class TextWidget : Widget { 13778 TextLayouter l; 13779 ScrollMessageWidget smw; 13780 TextDisplayHelper helper; 13781 this(TextLayouter l, Widget parent) { 13782 this.l = l; 13783 super(parent); 13784 13785 smw = new ScrollMessageWidget(this); 13786 //smw.horizontalScrollBar.hide; 13787 //smw.verticalScrollBar.hide; 13788 smw.addDefaultWheelListeners(16, 16, 8); 13789 smw.movementPerButtonClick(16, 16); 13790 helper = new TextDisplayHelper(l, smw); 13791 13792 // no need to do this here since there's gonna be a resize 13793 // event immediately before any drawing 13794 // smw.setTotalArea(l.width, l.height); 13795 smw.setViewableArea( 13796 this.width - this.paddingLeft - this.paddingRight, 13797 this.height - this.paddingTop - this.paddingBottom); 13798 13799 /+ 13800 writeln(l.width, "x", l.height); 13801 +/ 13802 } 13803 } 13804 +/ 13805 13806 13807 13808 13809 /+ 13810 make sure it calls parentWindow.inputProxy.setIMEPopupLocation too 13811 +/ 13812 13813 /++ 13814 Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit]. 13815 +/ 13816 abstract class EditableTextWidget : Widget { 13817 protected this(Widget parent) { 13818 version(custom_widgets) 13819 this(true, parent); 13820 else 13821 this(false, parent); 13822 } 13823 13824 private bool useCustomWidget; 13825 13826 protected this(bool useCustomWidget, Widget parent) { 13827 this.useCustomWidget = useCustomWidget; 13828 13829 super(parent); 13830 13831 if(useCustomWidget) 13832 setupCustomTextEditing(); 13833 } 13834 13835 private bool wordWrapEnabled_; 13836 /++ 13837 Enables or disables wrapping of long lines on word boundaries. 13838 +/ 13839 void wordWrapEnabled(bool enabled) { 13840 if(useCustomWidget) { 13841 wordWrapEnabled_ = enabled; 13842 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 13843 } else version(win32_widgets) { 13844 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 13845 } 13846 } 13847 13848 override int minWidth() { return scaleWithDpi(16); } 13849 override int widthStretchiness() { return 7; } 13850 override int widthShrinkiness() { return 1; } 13851 13852 override int maxHeight() { 13853 if(useCustomWidget) 13854 return tdh.maxHeight; 13855 else 13856 return super.maxHeight(); 13857 } 13858 13859 override void focus() { 13860 if(useCustomWidget && tdh) 13861 tdh.focus(); 13862 else 13863 super.focus(); 13864 } 13865 13866 override void defaultEventHandler_focusout(Event foe) { 13867 if(tdh !is null && foe.target is tdh) 13868 tdh.redraw(); 13869 } 13870 13871 override void defaultEventHandler_focusin(Event foe) { 13872 if(tdh !is null && foe.target is tdh) 13873 tdh.redraw(); 13874 } 13875 13876 13877 /++ 13878 Selects all the text in the control, as if the user did it themselves. When the user types in a widget, the selected text is replaced with the new input, so this might be useful for putting in default text that is easy for the user to replace. 13879 +/ 13880 void selectAll() { 13881 if(useCustomWidget) { 13882 tdh.selectAll(); 13883 } else version(win32_widgets) { 13884 SendMessage(hwnd, EM_SETSEL, 0, -1); 13885 } 13886 } 13887 13888 /++ 13889 Basic clipboard operations. 13890 13891 History: 13892 Added December 31, 2024 13893 +/ 13894 void copy() { 13895 if(useCustomWidget) { 13896 tdh.copy(); 13897 } else version(win32_widgets) { 13898 SendMessage(hwnd, WM_COPY, 0, 0); 13899 } 13900 } 13901 13902 /// ditto 13903 void cut() { 13904 if(useCustomWidget) { 13905 tdh.cut(); 13906 } else version(win32_widgets) { 13907 SendMessage(hwnd, WM_CUT, 0, 0); 13908 } 13909 } 13910 13911 /// ditto 13912 void paste() { 13913 if(useCustomWidget) { 13914 tdh.paste(); 13915 } else version(win32_widgets) { 13916 SendMessage(hwnd, WM_PASTE, 0, 0); 13917 } 13918 } 13919 13920 /// 13921 void undo() { 13922 if(useCustomWidget) { 13923 tdh.undo(); 13924 } else version(win32_widgets) { 13925 SendMessage(hwnd, EM_UNDO, 0, 0); 13926 } 13927 } 13928 13929 // note that WM_CLEAR deletes the selection without copying it to the clipboard 13930 // also windows supports margins, modified flag, and much more 13931 13932 // EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here 13933 13934 // EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc. 13935 13936 13937 13938 /*protected*/ TextDisplayHelper tdh; 13939 /*protected*/ TextLayouter textLayout; 13940 13941 /++ 13942 Gets or sets the current content of the control, as a plain text string. Setting the content will reset the cursor position and overwrite any changes the user made. 13943 +/ 13944 @property string content() { 13945 if(useCustomWidget) { 13946 return textLayout.getTextString(); 13947 } else version(win32_widgets) { 13948 wchar[4096] bufferstack; 13949 wchar[] buffer; 13950 auto len = GetWindowTextLength(hwnd); 13951 if(len < bufferstack.length) 13952 buffer = bufferstack[0 .. len + 1]; 13953 else 13954 buffer = new wchar[](len + 1); 13955 13956 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 13957 if(l >= 0) 13958 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 13959 else 13960 return null; 13961 } 13962 13963 assert(0); 13964 } 13965 /// ditto 13966 @property void content(string s) { 13967 if(useCustomWidget) { 13968 with(textLayout.selection) { 13969 moveToStartOfDocument(); 13970 setAnchor(); 13971 moveToEndOfDocument(); 13972 setFocus(); 13973 replaceContent(s); 13974 } 13975 13976 tdh.adjustScrollbarSizes(); 13977 // these don't seem to help 13978 // tdh.smw.setPosition(0, 0); 13979 // tdh.scrollForCaret(); 13980 13981 redraw(); 13982 } else version(win32_widgets) { 13983 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 13984 SetWindowTextW(hwnd, bfr.ptr); 13985 } 13986 } 13987 13988 /++ 13989 Appends some text to the widget at the end, without affecting the user selection or cursor position. 13990 +/ 13991 void addText(string txt) { 13992 if(useCustomWidget) { 13993 textLayout.appendText(txt); 13994 tdh.adjustScrollbarSizes(); 13995 redraw(); 13996 } else version(win32_widgets) { 13997 // get the current selection 13998 DWORD StartPos, EndPos; 13999 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 14000 14001 // move the caret to the end of the text 14002 int outLength = GetWindowTextLengthW(hwnd); 14003 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 14004 14005 // insert the text at the new caret position 14006 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 14007 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 14008 14009 // restore the previous selection 14010 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 14011 } 14012 } 14013 14014 // EM_SCROLLCARET scrolls the caret into view 14015 14016 void scrollToBottom() { 14017 if(useCustomWidget) { 14018 tdh.smw.scrollDown(int.max); 14019 } else version(win32_widgets) { 14020 SendMessageW( hwnd, EM_LINESCROLL, 0, int.max ); 14021 } 14022 } 14023 14024 protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14025 return new TextDisplayHelper(textLayout, smw); 14026 } 14027 14028 protected TextStyle defaultTextStyle() { 14029 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 14030 } 14031 14032 private OperatingSystemFont getUsedFont() { 14033 auto cs = getComputedStyle(); 14034 auto font = cs.font; 14035 if(font is null) { 14036 font = new OperatingSystemFont; 14037 font.loadDefault(); 14038 } 14039 return font; 14040 } 14041 14042 protected void setupCustomTextEditing() { 14043 textLayout = new TextLayouter(defaultTextStyle()); 14044 14045 auto smw = new ScrollMessageWidget(this); 14046 if(!showingHorizontalScroll) 14047 smw.horizontalScrollBar.hide(); 14048 if(!showingVerticalScroll) 14049 smw.verticalScrollBar.hide(); 14050 this.tabStop = false; 14051 smw.tabStop = false; 14052 tdh = textDisplayHelperFactory(textLayout, smw); 14053 } 14054 14055 override void newParentWindow(Window old, Window n) { 14056 if(n is null) return; 14057 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 14058 if(textLayout) { 14059 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 14060 // the dpi change can change the font, so this informs the layouter that it has changed too 14061 style.font_ = getUsedFont(); 14062 14063 // arsd.core.writeln(this.parentWindow.win.actualDpi); 14064 } 14065 } 14066 }); 14067 } 14068 14069 static class Style : Widget.Style { 14070 override WidgetBackground background() { 14071 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 14072 } 14073 14074 override Color foregroundColor() { 14075 return WidgetPainter.visualTheme.foregroundColor; 14076 } 14077 14078 override FrameStyle borderStyle() { 14079 return FrameStyle.sunk; 14080 } 14081 14082 override MouseCursor cursor() { 14083 return GenericCursor.Text; 14084 } 14085 } 14086 mixin OverrideStyle!Style; 14087 14088 version(win32_widgets) { 14089 private string lastContentBlur; 14090 14091 override void defaultEventHandler_blur(Event ev) { 14092 super.defaultEventHandler_blur(ev); 14093 14094 if(!useCustomWidget) 14095 if(this.content != lastContentBlur) { 14096 auto evt = new ChangeEvent!string(this, &this.content); 14097 evt.dispatch(); 14098 lastContentBlur = this.content; 14099 } 14100 } 14101 } 14102 14103 14104 bool showingVerticalScroll() { return true; } 14105 bool showingHorizontalScroll() { return true; } 14106 } 14107 14108 /++ 14109 A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`. 14110 14111 A `CustomLineEdit` always uses the custom implementation, even on operating systems where the native control is implemented in minigui, which may provide more api styling features but at the cost of poorer integration with the OS and potentially worse user experience in other ways. 14112 14113 See_Also: 14114 [PasswordEdit] for a `LineEdit` that obscures its input. 14115 14116 [TextEdit] for a multi-line plain text editor widget. 14117 14118 [TextLabel] for a single line piece of static text. 14119 14120 [TextDisplay] for a read-only display of a larger piece of plain text. 14121 +/ 14122 class LineEdit : EditableTextWidget { 14123 override bool showingVerticalScroll() { return false; } 14124 override bool showingHorizontalScroll() { return false; } 14125 14126 override int flexBasisWidth() { return 250; } 14127 override int widthShrinkiness() { return 10; } 14128 14129 /// 14130 this(Widget parent) { 14131 super(parent); 14132 version(win32_widgets) { 14133 createWin32Window(this, "edit"w, "", 14134 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 14135 } else version(custom_widgets) { 14136 } else static assert(false); 14137 } 14138 14139 private this(bool useCustomWidget, Widget parent) { 14140 if(!useCustomWidget) 14141 this(parent); 14142 else 14143 super(true, parent); 14144 } 14145 14146 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14147 auto tdh = new TextDisplayHelper(textLayout, smw); 14148 tdh.singleLine = true; 14149 return tdh; 14150 } 14151 14152 version(win32_widgets) { 14153 mixin Padding!q{0}; 14154 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 14155 override int maxHeight() { return minHeight; } 14156 } 14157 14158 /+ 14159 @property void passwordMode(bool p) { 14160 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 14161 } 14162 +/ 14163 } 14164 14165 /// ditto 14166 class CustomLineEdit : LineEdit { 14167 this(Widget parent) { 14168 super(true, parent); 14169 } 14170 } 14171 14172 /++ 14173 A [LineEdit] that displays `*` in place of the actual characters. 14174 14175 Alas, Windows requires the window to be created differently to use this style, 14176 so it had to be a new class instead of a toggle on and off on an existing object. 14177 14178 History: 14179 Added January 24, 2021 14180 14181 Implemented on Linux on January 31, 2023. 14182 +/ 14183 class PasswordEdit : EditableTextWidget { 14184 override bool showingVerticalScroll() { return false; } 14185 override bool showingHorizontalScroll() { return false; } 14186 14187 override int flexBasisWidth() { return 250; } 14188 14189 override TextStyle defaultTextStyle() { 14190 auto cs = getComputedStyle(); 14191 14192 auto osf = new class OperatingSystemFont { 14193 this() { 14194 super(cs.font); 14195 } 14196 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 14197 int count = 0; 14198 foreach(dchar ch; text) 14199 count++; 14200 return count * super.stringWidth("*", window); 14201 } 14202 }; 14203 14204 return new TextDisplayHelper.MyTextStyle(osf); 14205 } 14206 14207 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14208 static class TDH : TextDisplayHelper { 14209 this(TextLayouter textLayout, ScrollMessageWidget smw) { 14210 singleLine = true; 14211 super(textLayout, smw); 14212 } 14213 14214 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14215 char[256] buffer = void; 14216 int bufferLength = 0; 14217 foreach(dchar ch; text) 14218 buffer[bufferLength++] = '*'; 14219 painter.drawText(upperLeft, buffer[0..bufferLength]); 14220 } 14221 } 14222 14223 return new TDH(textLayout, smw); 14224 } 14225 14226 /// 14227 this(Widget parent) { 14228 super(parent); 14229 version(win32_widgets) { 14230 createWin32Window(this, "edit"w, "", 14231 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 14232 } else version(custom_widgets) { 14233 } else static assert(false); 14234 } 14235 14236 private this(bool useCustomWidget, Widget parent) { 14237 if(!useCustomWidget) 14238 this(parent); 14239 else 14240 super(true, parent); 14241 } 14242 14243 version(win32_widgets) { 14244 mixin Padding!q{2}; 14245 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 14246 override int maxHeight() { return minHeight; } 14247 } 14248 } 14249 14250 /// ditto 14251 class CustomPasswordEdit : PasswordEdit { 14252 this(Widget parent) { 14253 super(true, parent); 14254 } 14255 } 14256 14257 14258 /++ 14259 A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`. 14260 14261 See_Also: 14262 [TextDisplay] for a read-only text display. 14263 14264 [LineEdit] for a single line text editor. 14265 14266 [PasswordEdit] for a single line text editor that obscures its input. 14267 +/ 14268 class TextEdit : EditableTextWidget { 14269 /// 14270 this(Widget parent) { 14271 super(parent); 14272 version(win32_widgets) { 14273 createWin32Window(this, "edit"w, "", 14274 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 14275 } else version(custom_widgets) { 14276 } else static assert(false); 14277 } 14278 14279 private this(bool useCustomWidget, Widget parent) { 14280 if(!useCustomWidget) 14281 this(parent); 14282 else 14283 super(true, parent); 14284 } 14285 14286 override int maxHeight() { return int.max; } 14287 override int heightStretchiness() { return 7; } 14288 14289 override int flexBasisWidth() { return 250; } 14290 override int flexBasisHeight() { return 25; } 14291 } 14292 14293 /// ditto 14294 class CustomTextEdit : TextEdit { 14295 this(Widget parent) { 14296 super(true, parent); 14297 } 14298 } 14299 14300 /+ 14301 /++ 14302 14303 +/ 14304 version(none) 14305 class RichTextDisplay : Widget { 14306 @property void content(string c) {} 14307 void appendContent(string c) {} 14308 } 14309 +/ 14310 14311 /++ 14312 A read-only text display. It is based on the editable widget base, but does not allow user edits and displays it on the direct background instead of on an editable background. 14313 14314 History: 14315 Added October 31, 2023 (dub v11.3) 14316 +/ 14317 class TextDisplay : EditableTextWidget { 14318 this(string text, Widget parent) { 14319 super(true, parent); 14320 this.content = text; 14321 } 14322 14323 override int maxHeight() { return int.max; } 14324 override int minHeight() { return Window.defaultLineHeight; } 14325 override int heightStretchiness() { return 7; } 14326 override int heightShrinkiness() { return 2; } 14327 14328 override int flexBasisWidth() { 14329 return scaleWithDpi(250); 14330 } 14331 override int flexBasisHeight() { 14332 if(textLayout is null || this.tdh is null) 14333 return Window.defaultLineHeight; 14334 14335 auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height; 14336 return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height; 14337 } 14338 14339 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14340 return new MyTextDisplayHelper(textLayout, smw); 14341 } 14342 14343 override void registerMovement() { 14344 super.registerMovement(); 14345 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 14346 } 14347 14348 static class MyTextDisplayHelper : TextDisplayHelper { 14349 this(TextLayouter textLayout, ScrollMessageWidget smw) { 14350 smw.verticalScrollBar.hide(); 14351 smw.horizontalScrollBar.hide(); 14352 super(textLayout, smw); 14353 this.readonly = true; 14354 } 14355 14356 override void registerMovement() { 14357 super.registerMovement(); 14358 14359 // FIXME: do the horizontal one too as needed and make sure that it does 14360 // wordwrapping again 14361 if(l.height + smw.horizontalScrollBar.height > this.height) 14362 smw.verticalScrollBar.show(); 14363 else 14364 smw.verticalScrollBar.hide(); 14365 14366 l.wordWrapWidth = this.width; 14367 14368 smw.verticalScrollBar.setPosition = 0; 14369 } 14370 } 14371 14372 class Style : Widget.Style { 14373 // just want the generic look for these 14374 } 14375 14376 mixin OverrideStyle!Style; 14377 } 14378 14379 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 14380 /++ 14381 A scrollable viewer for an array of widgets. The widgets inside a list item can be whatever you want, and you can have any number of total items you want because only the visible widgets need to actually exist and load their data at a time, giving constantly predictable performance. 14382 14383 14384 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 14385 14386 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 14387 14388 Note that some state in reused widget objects may either be preserved or reset when the user isn't expecting it. It is your responsibility to handle this when you load an item (try to save it when it is unloaded, then set it when reloaded), but my recommendation would be to have minimal extra state. For example, avoid having a scrollable widget inside a list, since the scroll state might change as it goes out and into view. Instead, I'd suggest making the list be a loader for a details pane on the side. 14389 14390 History: 14391 Added August 12, 2024 (dub v11.6) 14392 +/ 14393 abstract class GenericListViewWidget : Widget { 14394 /++ 14395 14396 +/ 14397 this(Widget parent) { 14398 super(parent); 14399 14400 smw = new ScrollMessageWidget(this); 14401 smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width); 14402 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 14403 smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet 14404 14405 inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw)); 14406 inner.tabStop = this.tabStop; 14407 this.tabStop = false; 14408 } 14409 14410 private ScrollMessageWidget smw; 14411 private GenericListViewWidgetInner inner; 14412 14413 /++ 14414 14415 +/ 14416 abstract GenericListViewItem itemFactory(Widget parent); 14417 // in device-dependent pixels 14418 /++ 14419 14420 +/ 14421 abstract Size itemSize(); // use 0 to indicate it can stretch? 14422 14423 enum LayoutMode { 14424 rows, 14425 columns, 14426 gridRowsFirst, 14427 gridColumnsFirst 14428 } 14429 LayoutMode layoutMode() { 14430 return LayoutMode.rows; 14431 } 14432 14433 private int itemCount_; 14434 14435 /++ 14436 Sets the count of available items in the list. This will not allocate any items, but it will adjust the scroll bars and try to load items up to this count on-demand as they appear visible. 14437 +/ 14438 void setItemCount(int count) { 14439 smw.setTotalArea(inner.width, count * itemSize().height); 14440 smw.setViewableArea(inner.width, inner.height); 14441 this.itemCount_ = count; 14442 } 14443 14444 /++ 14445 Returns the current count of items expected to available in the list. 14446 +/ 14447 int itemCount() { 14448 return this.itemCount_; 14449 } 14450 14451 /++ 14452 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 14453 14454 Note you must $(I also) call [setItemCount] if the total item count has changed. 14455 +/ 14456 void notifyItemsChanged(int index, int count = 1) { 14457 } 14458 /// ditto 14459 void notifyItemsInserted(int index, int count = 1) { 14460 } 14461 /// ditto 14462 void notifyItemsRemoved(int index, int count = 1) { 14463 } 14464 /// ditto 14465 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 14466 } 14467 14468 /++ 14469 History: 14470 Added January 1, 2025 14471 +/ 14472 void ensureItemVisibleInScroll(int index) { 14473 auto itemPos = index * itemSize().height; 14474 auto vsb = smw.verticalScrollBar; 14475 auto viewable = vsb.viewableArea_; 14476 14477 if(viewable == 0) { 14478 // viewable == 0 isn't actually supposed to happen, this means 14479 // this method is being called before having our size assigned, it should 14480 // probably just queue it up for later. 14481 queuedScroll = index; 14482 return; 14483 } 14484 14485 queuedScroll = int.min; 14486 14487 if(itemPos < vsb.position) { 14488 // scroll up to it 14489 vsb.setPosition(itemPos); 14490 smw.notify(); 14491 } else if(itemPos + itemSize().height > (vsb.position + viewable)) { 14492 // scroll down to it, so it is at the bottom 14493 14494 auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height; 14495 // need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it 14496 14497 vsb.setPosition(itemPos - lastViewableItemPosition); 14498 smw.notify(); 14499 } 14500 } 14501 14502 /++ 14503 History: 14504 Added January 1, 2025; 14505 +/ 14506 int numberOfCurrentlyFullyVisibleItems() { 14507 return smw.verticalScrollBar.viewableArea_ / itemSize.height; 14508 } 14509 14510 private int queuedScroll = int.min; 14511 14512 override void recomputeChildLayout() { 14513 super.recomputeChildLayout(); 14514 if(queuedScroll != int.min) 14515 ensureItemVisibleInScroll(queuedScroll); 14516 } 14517 14518 private GenericListViewItem[] items; 14519 14520 override void paint(WidgetPainter painter) {} 14521 } 14522 14523 /// ditto 14524 abstract class GenericListViewItem : Widget { 14525 /++ 14526 +/ 14527 this(Widget parent) { 14528 super(parent); 14529 } 14530 14531 private int _currentIndex = -1; 14532 14533 private void showItemPrivate(int idx) { 14534 showItem(idx); 14535 _currentIndex = idx; 14536 } 14537 14538 /++ 14539 Implement this to show an item from your data backing to the list. 14540 14541 Note that even if you are showing the requested index already, you should still try to reload it because it is possible the index now points to a different item (e.g. an item was added so all the indexes have changed) or if data has changed in this index and it is requesting you to update it prior to a repaint. 14542 +/ 14543 abstract void showItem(int idx); 14544 14545 /++ 14546 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 14547 14548 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 14549 14550 Inside the call to `showItem`, `currentIndexLoaded` is the old index, and the argument to `showItem` is the new index. You might use that to save state to the right place as needed before you overwrite it with the new item. 14551 +/ 14552 final int currentIndexLoaded() { 14553 return _currentIndex; 14554 } 14555 } 14556 14557 /// 14558 unittest { 14559 import arsd.minigui; 14560 14561 import std.conv; 14562 14563 void main() { 14564 auto mw = new MainWindow(); 14565 14566 static class MyListViewItem : GenericListViewItem { 14567 this(Widget parent) { 14568 super(parent); 14569 14570 label = new TextLabel("unloaded", TextAlignment.Left, this); 14571 button = new Button("Click", this); 14572 14573 button.addEventListener("triggered", (){ 14574 messageBox(text("clicked ", currentIndexLoaded())); 14575 }); 14576 } 14577 override void showItem(int idx) { 14578 label.label = "Item " ~ to!string(idx); 14579 } 14580 14581 TextLabel label; 14582 Button button; 14583 } 14584 14585 auto widget = new class GenericListViewWidget { 14586 this() { 14587 super(mw); 14588 } 14589 override GenericListViewItem itemFactory(Widget parent) { 14590 return new MyListViewItem(parent); 14591 } 14592 override Size itemSize() { 14593 return Size(0, scaleWithDpi(80)); 14594 } 14595 }; 14596 14597 widget.setItemCount(5000); 14598 14599 mw.loop(); 14600 } 14601 } 14602 14603 // this exists just to wrap the actual GenericListViewWidgetInner so borders 14604 // and padding and stuff can work 14605 private class GenericListViewInnerContainer : Widget { 14606 this(Widget parent) { 14607 super(parent); 14608 this.tabStop = false; 14609 } 14610 14611 override void recomputeChildLayout() { 14612 registerMovement(); 14613 14614 auto cs = getComputedStyle(); 14615 auto bw = getBorderWidth(cs.borderStyle); 14616 14617 assert(children.length < 2); 14618 foreach(child; children) { 14619 child.x = bw + paddingLeft(); 14620 child.y = bw + paddingTop(); 14621 child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight(); 14622 child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom(); 14623 14624 child.recomputeChildLayout(); 14625 } 14626 } 14627 14628 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 14629 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14630 return parent.parent.parent.useStyleProperties(dg); 14631 else 14632 return super.useStyleProperties(dg); 14633 } 14634 14635 override int paddingTop() { 14636 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14637 return parent.parent.parent.paddingTop(); 14638 else 14639 return super.paddingTop(); 14640 } 14641 14642 override int paddingBottom() { 14643 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14644 return parent.parent.parent.paddingBottom(); 14645 else 14646 return super.paddingBottom(); 14647 } 14648 14649 override int paddingLeft() { 14650 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14651 return parent.parent.parent.paddingLeft(); 14652 else 14653 return super.paddingLeft(); 14654 } 14655 14656 override int paddingRight() { 14657 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14658 return parent.parent.parent.paddingRight(); 14659 else 14660 return super.paddingRight(); 14661 } 14662 14663 14664 } 14665 14666 private class GenericListViewWidgetInner : Widget { 14667 this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) { 14668 super(parent); 14669 this.glvw = glvw; 14670 14671 reloadVisible(); 14672 14673 smw.addEventListener("scroll", () { 14674 reloadVisible(); 14675 }); 14676 } 14677 14678 override void registerMovement() { 14679 super.registerMovement(); 14680 if(glvw && glvw.smw) 14681 glvw.smw.setViewableArea(this.width, this.height); 14682 } 14683 14684 void reloadVisible() { 14685 auto y = glvw.smw.position.y / glvw.itemSize.height; 14686 14687 // idk why i had this here it doesn't seem to be ueful and actually made last items diasppear 14688 //int offset = glvw.smw.position.y % glvw.itemSize.height; 14689 //if(offset || y >= glvw.itemCount()) 14690 //y--; 14691 14692 if(y < 0) 14693 y = 0; 14694 14695 recomputeChildLayout(); 14696 14697 foreach(item; glvw.items) { 14698 if(y < glvw.itemCount()) { 14699 item.showItemPrivate(y); 14700 item.show(); 14701 } else { 14702 item.hide(); 14703 } 14704 y++; 14705 } 14706 14707 this.redraw(); 14708 } 14709 14710 private GenericListViewWidget glvw; 14711 14712 private bool inRcl; 14713 override void recomputeChildLayout() { 14714 if(inRcl) 14715 return; 14716 inRcl = true; 14717 scope(exit) 14718 inRcl = false; 14719 14720 registerMovement(); 14721 14722 auto ih = glvw.itemSize().height; 14723 14724 auto itemCount = this.height / ih + 2; // extra for partial display before and after 14725 bool hadNew; 14726 while(glvw.items.length < itemCount) { 14727 // FIXME: free the old items? maybe just set length 14728 glvw.items ~= glvw.itemFactory(this); 14729 hadNew = true; 14730 } 14731 14732 if(hadNew) 14733 reloadVisible(); 14734 14735 int y = -(glvw.smw.position.y % ih) + this.paddingTop(); 14736 foreach(child; children) { 14737 child.x = this.paddingLeft(); 14738 child.y = y; 14739 y += glvw.itemSize().height; 14740 child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight(); 14741 child.height = ih; 14742 14743 child.recomputeChildLayout(); 14744 } 14745 } 14746 } 14747 14748 14749 14750 /++ 14751 History: 14752 It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`. 14753 +/ 14754 class MessageBox : Dialog { 14755 private string message; 14756 MessageBoxButton buttonPressed = MessageBoxButton.None; 14757 /++ 14758 14759 History: 14760 The overload that takes `Window originator` was added on September 29, 2024. 14761 +/ 14762 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 14763 this(null, message, buttons, buttonIds); 14764 } 14765 /// ditto 14766 this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 14767 message = message.stripRightInternal; 14768 int mainWidth; 14769 14770 // estimate longest line 14771 int count; 14772 foreach(ch; message) { 14773 if(ch == '\n') { 14774 if(count > mainWidth) 14775 mainWidth = count; 14776 count = 0; 14777 } else { 14778 count++; 14779 } 14780 } 14781 mainWidth *= 8; 14782 if(mainWidth < 300) 14783 mainWidth = 300; 14784 if(mainWidth > 600) 14785 mainWidth = 600; 14786 14787 super(originator, mainWidth, 100); 14788 14789 assert(buttons.length); 14790 assert(buttons.length == buttonIds.length); 14791 14792 this.message = message; 14793 14794 auto label = new TextDisplay(message, this); 14795 14796 auto hl = new HorizontalLayout(this); 14797 auto spacer = new HorizontalSpacer(hl); // to right align 14798 14799 foreach(idx, buttonText; buttons) { 14800 auto button = new CommandButton(buttonText, hl); 14801 14802 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 14803 this.buttonPressed = buttonIds[idx]; 14804 win.close(); 14805 }; })(idx)); 14806 14807 if(idx == 0) 14808 button.focus(); 14809 } 14810 14811 if(buttons.length == 1) 14812 auto spacer2 = new HorizontalSpacer(hl); // to center it 14813 14814 auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom; 14815 auto max = scaleWithDpi(600); // random max height 14816 if(size > max) 14817 size = max; 14818 14819 win.resize(scaleWithDpi(mainWidth), size); 14820 14821 win.show(); 14822 redraw(); 14823 } 14824 14825 override void OK() { 14826 this.win.close(); 14827 } 14828 14829 mixin Padding!q{16}; 14830 } 14831 14832 /// 14833 enum MessageBoxStyle { 14834 OK, /// 14835 OKCancel, /// 14836 RetryCancel, /// 14837 YesNo, /// 14838 YesNoCancel, /// 14839 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. 14840 } 14841 14842 /// 14843 enum MessageBoxIcon { 14844 None, /// 14845 Info, /// 14846 Warning, /// 14847 Error /// 14848 } 14849 14850 /// Identifies the button the user pressed on a message box. 14851 enum MessageBoxButton { 14852 None, /// The user closed the message box without clicking any of the buttons. 14853 OK, /// 14854 Cancel, /// 14855 Retry, /// 14856 Yes, /// 14857 No, /// 14858 Continue /// 14859 } 14860 14861 14862 /++ 14863 Displays a modal message box, blocking until the user dismisses it. These global ones are discouraged in favor of the same methods on [Window], which give better user experience since the message box is tied the parent window instead of acting independently. 14864 14865 Returns: the button pressed. 14866 +/ 14867 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 14868 return messageBox(null, title, message, style, icon); 14869 } 14870 14871 /// ditto 14872 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 14873 return messageBox(null, null, message, style, icon); 14874 } 14875 14876 /++ 14877 14878 +/ 14879 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 14880 version(win32_widgets) { 14881 WCharzBuffer t = WCharzBuffer(title); 14882 WCharzBuffer m = WCharzBuffer(message); 14883 UINT type; 14884 with(MessageBoxStyle) 14885 final switch(style) { 14886 case OK: type |= MB_OK; break; 14887 case OKCancel: type |= MB_OKCANCEL; break; 14888 case RetryCancel: type |= MB_RETRYCANCEL; break; 14889 case YesNo: type |= MB_YESNO; break; 14890 case YesNoCancel: type |= MB_YESNOCANCEL; break; 14891 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 14892 } 14893 with(MessageBoxIcon) 14894 final switch(icon) { 14895 case None: break; 14896 case Info: type |= MB_ICONINFORMATION; break; 14897 case Warning: type |= MB_ICONWARNING; break; 14898 case Error: type |= MB_ICONERROR; break; 14899 } 14900 switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) { 14901 case IDOK: return MessageBoxButton.OK; 14902 case IDCANCEL: return MessageBoxButton.Cancel; 14903 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 14904 case IDYES: return MessageBoxButton.Yes; 14905 case IDNO: return MessageBoxButton.No; 14906 case IDCONTINUE: return MessageBoxButton.Continue; 14907 default: return MessageBoxButton.None; 14908 } 14909 } else { 14910 string[] buttons; 14911 MessageBoxButton[] buttonIds; 14912 with(MessageBoxStyle) 14913 final switch(style) { 14914 case OK: 14915 buttons = ["OK"]; 14916 buttonIds = [MessageBoxButton.OK]; 14917 break; 14918 case OKCancel: 14919 buttons = ["OK", "Cancel"]; 14920 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 14921 break; 14922 case RetryCancel: 14923 buttons = ["Retry", "Cancel"]; 14924 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 14925 break; 14926 case YesNo: 14927 buttons = ["Yes", "No"]; 14928 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 14929 break; 14930 case YesNoCancel: 14931 buttons = ["Yes", "No", "Cancel"]; 14932 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 14933 break; 14934 case RetryCancelContinue: 14935 buttons = ["Try Again", "Cancel", "Continue"]; 14936 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 14937 break; 14938 } 14939 auto mb = new MessageBox(originator, message, buttons, buttonIds); 14940 EventLoop el = EventLoop.get; 14941 el.run(() { return !mb.win.closed; }); 14942 return mb.buttonPressed; 14943 } 14944 14945 } 14946 14947 /// ditto 14948 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 14949 return messageBox(originator, null, message, style, icon); 14950 } 14951 14952 14953 /// 14954 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 14955 14956 /++ 14957 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 14958 14959 History: 14960 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. 14961 +/ 14962 struct EventListener { 14963 private Widget widget; 14964 private string event; 14965 private EventHandler handler; 14966 private bool useCapture; 14967 14968 /// 14969 void disconnect() { 14970 widget.removeEventListener(this); 14971 } 14972 } 14973 14974 /++ 14975 The purpose of this enum was to give a compile-time checked version of various standard event strings. 14976 14977 Now, I recommend you use a statically typed event object instead. 14978 14979 See_Also: [Event] 14980 +/ 14981 enum EventType : string { 14982 click = "click", /// 14983 14984 mouseenter = "mouseenter", /// 14985 mouseleave = "mouseleave", /// 14986 mousein = "mousein", /// 14987 mouseout = "mouseout", /// 14988 mouseup = "mouseup", /// 14989 mousedown = "mousedown", /// 14990 mousemove = "mousemove", /// 14991 14992 keydown = "keydown", /// 14993 keyup = "keyup", /// 14994 char_ = "char", /// 14995 14996 focus = "focus", /// 14997 blur = "blur", /// 14998 14999 triggered = "triggered", /// 15000 15001 change = "change", /// 15002 } 15003 15004 /++ 15005 Represents an event that is currently being processed. 15006 15007 15008 Minigui's event model is based on the web browser. An event has a name, a target, 15009 and an associated data object. It starts from the window and works its way down through 15010 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 15011 then goes back up again all the way back to the window, triggering bubble phase handlers. At 15012 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 15013 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 15014 was called; that just stops it from calling other handlers in the widget tree, but the default happens 15015 whenever propagation is done, not only if it gets to the end of the chain). 15016 15017 This model has several nice points: 15018 15019 $(LIST 15020 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 15021 with event handlers set, then add/remove children as much as you want without needing 15022 to manage the event handlers on them - the parent alone can manage everything. 15023 15024 * It is easy to create new custom events in your application. 15025 15026 * It is familiar to many web developers. 15027 ) 15028 15029 There's a few downsides though: 15030 15031 $(LIST 15032 * There's not a lot of type safety. 15033 15034 * You don't get a static list of what events a widget can emit. 15035 15036 * Tracing where an event got cancelled along the chain can get difficult; the downside of 15037 the central delegation benefit is it can be lead to debugging of action at a distance. 15038 ) 15039 15040 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 15041 while keeping the benefits - and most compatibility with - the existing model. The main idea is 15042 to simply use a D object type which provides a static interface as well as a built-in event name. 15043 Then, a new static interface allows you to see what an event can emit and attach handlers to it 15044 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 15045 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 15046 to having a little more help from the D compiler and documentation generator. 15047 15048 Your code would change like this: 15049 15050 --- 15051 // old 15052 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 15053 15054 // new 15055 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 15056 --- 15057 15058 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. 15059 15060 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 15061 15062 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 15063 15064 Thus the family of functions are: 15065 15066 [Widget.addEventListener] is the fully-flexible base method. It has two main overload families: one with the string and one without. The one with the string takes the Event object, the one without determines the string from the type you pass. The string "*" matches ALL events that pass through. 15067 15068 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 15069 15070 [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. 15071 15072 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 15073 15074 --- 15075 class MyCheckbox : Widget { 15076 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 15077 /// It is NOT actually required but should be used whenever possible. 15078 mixin Emits!(ChangeEvent!bool); 15079 15080 this(Widget parent) { 15081 super(parent); 15082 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 15083 } 15084 15085 private bool _checked; 15086 @property bool checked() { return _checked; } 15087 @property void checked(bool set) { 15088 _checked = set; 15089 emit!(ChangeEvent!bool)(&checked); 15090 } 15091 } 15092 --- 15093 15094 ## Creating Your Own Events 15095 15096 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. 15097 15098 --- 15099 class MyEvent : Event { 15100 this(Widget target) { super(EventString, target); } 15101 mixin Register; // adds EventString and other reflection information 15102 } 15103 --- 15104 15105 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 15106 15107 History: 15108 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. 15109 15110 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. 15111 +/ 15112 /+ 15113 15114 ## General Conventions 15115 15116 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. 15117 15118 15119 ## Qt-style signals and slots 15120 15121 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. 15122 15123 The intention is for events to be used when 15124 15125 --- 15126 class Demo : Widget { 15127 this() { 15128 myPropertyChanged = Signal!int(this); 15129 } 15130 @property myProperty(int v) { 15131 myPropertyChanged.emit(v); 15132 } 15133 15134 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 15135 // but it can just genuinely not care about `this` since that's not really passed. 15136 } 15137 15138 class Foo : Widget { 15139 // the slot uda is not necessary, but it helps the script and ui builder find it. 15140 @slot void setValue(int v) { ... } 15141 } 15142 15143 demo.myPropertyChanged.connect(&foo.setValue); 15144 --- 15145 15146 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 15147 15148 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 15149 15150 class StringChangeEvent : ChangeEvent, Signal!string { 15151 mixin SignalImpl 15152 } 15153 15154 +/ 15155 class Event : ReflectableProperties { 15156 /// Creates an event without populating any members and without sending it. See [dispatch] 15157 this(string eventName, Widget emittedBy) { 15158 this.eventName = eventName; 15159 this.srcElement = emittedBy; 15160 } 15161 15162 15163 /// Implementations for the [ReflectableProperties] interface/ 15164 void getPropertiesList(scope void delegate(string name) sink) const {} 15165 /// ditto 15166 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 15167 /// ditto 15168 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 15169 return SetPropertyResult.notPermitted; 15170 } 15171 15172 15173 /+ 15174 /++ 15175 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 15176 15177 It is just protected so the mixin template can see it from user modules. If I made it private, even my own mixin template couldn't see it due to mixin scoping rules. 15178 +/ 15179 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 15180 if(value.length == 0) { 15181 finalSink(memberName, `""`); 15182 return; 15183 } 15184 15185 char[1024] bufferBacking; 15186 char[] buffer = bufferBacking; 15187 int bufferPosition; 15188 15189 void sink(char ch) { 15190 if(bufferPosition >= buffer.length) 15191 buffer.length = buffer.length + 1024; 15192 buffer[bufferPosition++] = ch; 15193 } 15194 15195 sink('"'); 15196 15197 foreach(ch; value) { 15198 switch(ch) { 15199 case '\\': 15200 sink('\\'); sink('\\'); 15201 break; 15202 case '"': 15203 sink('\\'); sink('"'); 15204 break; 15205 case '\n': 15206 sink('\\'); sink('n'); 15207 break; 15208 case '\r': 15209 sink('\\'); sink('r'); 15210 break; 15211 case '\t': 15212 sink('\\'); sink('t'); 15213 break; 15214 default: 15215 sink(ch); 15216 } 15217 } 15218 15219 sink('"'); 15220 15221 finalSink(memberName, buffer[0 .. bufferPosition]); 15222 } 15223 +/ 15224 15225 /+ 15226 enum EventInitiator { 15227 system, 15228 minigui, 15229 user 15230 } 15231 15232 immutable EventInitiator; initiatedBy; 15233 +/ 15234 15235 /++ 15236 Events should generally follow the propagation model, but there's some exceptions 15237 to that rule. If so, they should override this to return false. In that case, only 15238 bubbling event handlers on the target itself and capturing event handlers on the containing 15239 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 15240 capture -> target -> bubble process.) 15241 15242 History: 15243 Added May 12, 2021 15244 +/ 15245 bool propagates() const pure nothrow @nogc @safe { 15246 return true; 15247 } 15248 15249 /++ 15250 hints as to whether preventDefault will actually do anything. not entirely reliable. 15251 15252 History: 15253 Added May 14, 2021 15254 +/ 15255 bool cancelable() const pure nothrow @nogc @safe { 15256 return true; 15257 } 15258 15259 /++ 15260 You can mix this into child class to register some boilerplate. It includes the `EventString` 15261 member, a constructor, and implementations of the dynamic get data interfaces. 15262 15263 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 15264 15265 15266 You can override the default EventString by simply providing your own in the form of 15267 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 15268 which provides some namespace protection against conflicts in other libraries while still being fairly 15269 easy to use. 15270 15271 If you provide your own constructor, it will override the default constructor provided here. A constructor 15272 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 15273 first argument to your constructor. 15274 15275 History: 15276 Added May 13, 2021. 15277 +/ 15278 protected static mixin template Register() { 15279 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 15280 this(Widget target) { super(EventString, target); } 15281 15282 mixin ReflectableProperties.RegisterGetters; 15283 } 15284 15285 /++ 15286 This is the widget that emitted the event. 15287 15288 15289 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 15290 15291 History: 15292 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 15293 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 15294 so I don't intend to remove these aliases. 15295 +/ 15296 Widget source; 15297 /// ditto 15298 alias source target; 15299 /// ditto 15300 alias source srcElement; 15301 15302 Widget relatedTarget; /// Note: likely to be deprecated at some point. 15303 15304 /// Prevents the default event handler (if there is one) from being called 15305 void preventDefault() { 15306 lastDefaultPrevented = true; 15307 defaultPrevented = true; 15308 } 15309 15310 /// Stops the event propagation immediately. 15311 void stopPropagation() { 15312 propagationStopped = true; 15313 } 15314 15315 private bool defaultPrevented; 15316 private bool propagationStopped; 15317 private string eventName; 15318 15319 private bool isBubbling; 15320 15321 /// This is an internal implementation detail you should not use. It would be private if the language allowed it and it may be removed without notice. 15322 protected void adjustScrolling() { } 15323 /// ditto 15324 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 15325 15326 /++ 15327 this sends it only to the target. If you want propagation, use dispatch() instead. 15328 15329 This should be made private!!! 15330 15331 +/ 15332 void sendDirectly() { 15333 if(srcElement is null) 15334 return; 15335 15336 // 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. 15337 15338 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 15339 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 15340 15341 adjustScrolling(); 15342 15343 if(auto e = target.parentWindow) { 15344 if(auto handlers = "*" in e.capturingEventHandlers) 15345 foreach(handler; *handlers) 15346 if(handler) handler(e, this); 15347 if(auto handlers = eventName in e.capturingEventHandlers) 15348 foreach(handler; *handlers) 15349 if(handler) handler(e, this); 15350 } 15351 15352 auto e = srcElement; 15353 15354 if(auto handlers = eventName in e.bubblingEventHandlers) 15355 foreach(handler; *handlers) 15356 if(handler) handler(e, this); 15357 15358 if(auto handlers = "*" in e.bubblingEventHandlers) 15359 foreach(handler; *handlers) 15360 if(handler) handler(e, this); 15361 15362 // there's never a default for a catch-all event 15363 if(!defaultPrevented) 15364 if(eventName in e.defaultEventHandlers) 15365 e.defaultEventHandlers[eventName](e, this); 15366 } 15367 15368 /// this dispatches the element using the capture -> target -> bubble process 15369 void dispatch() { 15370 if(srcElement is null) 15371 return; 15372 15373 if(!propagates) { 15374 sendDirectly; 15375 return; 15376 } 15377 15378 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 15379 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 15380 15381 adjustScrolling(); 15382 // first capture, then bubble 15383 15384 Widget[] chain; 15385 Widget curr = srcElement; 15386 while(curr) { 15387 auto l = curr; 15388 chain ~= l; 15389 curr = curr.parent; 15390 } 15391 15392 isBubbling = false; 15393 15394 foreach_reverse(e; chain) { 15395 if(auto handlers = "*" in e.capturingEventHandlers) 15396 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15397 15398 if(propagationStopped) 15399 break; 15400 15401 if(auto handlers = eventName in e.capturingEventHandlers) 15402 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15403 15404 // the default on capture should really be to always do nothing 15405 15406 //if(!defaultPrevented) 15407 // if(eventName in e.defaultEventHandlers) 15408 // e.defaultEventHandlers[eventName](e.element, this); 15409 15410 if(propagationStopped) 15411 break; 15412 } 15413 15414 int adjustX; 15415 int adjustY; 15416 15417 isBubbling = true; 15418 if(!propagationStopped) 15419 foreach(e; chain) { 15420 if(auto handlers = eventName in e.bubblingEventHandlers) 15421 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15422 15423 if(propagationStopped) 15424 break; 15425 15426 if(auto handlers = "*" in e.bubblingEventHandlers) 15427 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15428 15429 if(propagationStopped) 15430 break; 15431 15432 if(e.encapsulatedChildren()) { 15433 adjustClientCoordinates(adjustX, adjustY); 15434 target = e; 15435 } else { 15436 adjustX += e.x; 15437 adjustY += e.y; 15438 } 15439 } 15440 15441 if(!defaultPrevented) 15442 foreach(e; chain) { 15443 if(eventName in e.defaultEventHandlers) 15444 e.defaultEventHandlers[eventName](e, this); 15445 } 15446 } 15447 15448 15449 /* old compatibility things */ 15450 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 15451 final @property { 15452 Key key() { return (cast(KeyEventBase) this).key; } 15453 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 15454 15455 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 15456 bool altKey() { return (cast(KeyEventBase) this).altKey; } 15457 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 15458 } 15459 15460 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 15461 final @property { 15462 int clientX() { return (cast(MouseEventBase) this).clientX; } 15463 int clientY() { return (cast(MouseEventBase) this).clientY; } 15464 15465 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 15466 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 15467 15468 int button() { return (cast(MouseEventBase) this).button; } 15469 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 15470 } 15471 15472 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 15473 final @property { 15474 int state() { 15475 if(auto meb = cast(MouseEventBase) this) 15476 return meb.state; 15477 if(auto keb = cast(KeyEventBase) this) 15478 return keb.state; 15479 assert(0); 15480 } 15481 } 15482 15483 deprecated("Use a CharEvent instead of Event in your handler going forward") 15484 final @property { 15485 dchar character() { 15486 if(auto ce = cast(CharEvent) this) 15487 return ce.character; 15488 return dchar.init; 15489 } 15490 } 15491 15492 // for change events 15493 @property { 15494 /// 15495 int intValue() { return 0; } 15496 /// 15497 string stringValue() { return null; } 15498 } 15499 } 15500 15501 /++ 15502 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 15503 15504 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 15505 dynamic and custom events, but the static list helps ensure you get them right. 15506 15507 If this is declared, you can use [Widget.emit] to send the event. 15508 15509 All events work the same way though, following the capture->widget->bubble model described under [Event]. 15510 15511 History: 15512 Added May 4, 2021 15513 +/ 15514 mixin template Emits(EventType) { 15515 import arsd.minigui : EventString; 15516 static if(is(EventType : Event) && !is(EventType == Event)) 15517 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 15518 else 15519 static assert(0, "You can only emit subclasses of Event"); 15520 } 15521 15522 /// ditto 15523 mixin template Emits(string eventString) { 15524 mixin("private Event[0] emits_" ~ eventString ~";"); 15525 } 15526 15527 /* 15528 class SignalEvent(string name) : Event { 15529 15530 } 15531 */ 15532 15533 /++ 15534 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". 15535 15536 15537 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. 15538 15539 History: 15540 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 15541 +/ 15542 class CommandEvent : Event { 15543 enum EventString = "command"; 15544 this(Widget source, string CommandString = EventString) { 15545 super(CommandString, source); 15546 } 15547 } 15548 15549 /++ 15550 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 15551 +/ 15552 class CommandEventWithArgs(Args...) : CommandEvent { 15553 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 15554 Args args; 15555 } 15556 15557 /++ 15558 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. 15559 15560 See [CommandEvent] for more information. 15561 15562 Returns: 15563 The [EventListener] you can use to remove the handler. 15564 +/ 15565 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 15566 return w.addEventListener(CommandString, (Event ev) { 15567 if(ev.target is w) 15568 return; // it does not consume its own commands! 15569 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 15570 handler(cev.args); 15571 ev.stopPropagation(); 15572 } 15573 }); 15574 } 15575 15576 /++ 15577 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. 15578 +/ 15579 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 15580 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 15581 event.dispatch(); 15582 } 15583 15584 /++ 15585 15586 +/ 15587 class ResizeEvent : Event { 15588 enum EventString = "resize"; 15589 15590 this(Widget target) { super(EventString, target); } 15591 15592 override bool propagates() const { return false; } 15593 } 15594 15595 /++ 15596 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 15597 15598 ClosedEvent happens when the window has been closed. It is already gone by the time this event fires, meaning you cannot prevent the close. Use [ClosingEvent] if you want to cancel, use [ClosedEvent] if you simply want to be notified. 15599 15600 History: 15601 Added June 21, 2021 (dub v10.1) 15602 +/ 15603 class ClosingEvent : Event { 15604 enum EventString = "closing"; 15605 15606 this(Widget target) { super(EventString, target); } 15607 15608 override bool propagates() const { return false; } 15609 override bool cancelable() const { return true; } 15610 } 15611 15612 /// ditto 15613 class ClosedEvent : Event { 15614 enum EventString = "closed"; 15615 15616 this(Widget target) { super(EventString, target); } 15617 15618 override bool propagates() const { return false; } 15619 override bool cancelable() const { return false; } 15620 } 15621 15622 /// 15623 class BlurEvent : Event { 15624 enum EventString = "blur"; 15625 15626 // FIXME: related target? 15627 this(Widget target) { super(EventString, target); } 15628 15629 override bool propagates() const { return false; } 15630 } 15631 15632 /// 15633 class FocusEvent : Event { 15634 enum EventString = "focus"; 15635 15636 // FIXME: related target? 15637 this(Widget target) { super(EventString, target); } 15638 15639 override bool propagates() const { return false; } 15640 } 15641 15642 /++ 15643 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 15644 15645 History: 15646 Added July 3, 2021 15647 +/ 15648 class FocusInEvent : Event { 15649 enum EventString = "focusin"; 15650 15651 // FIXME: related target? 15652 this(Widget target) { super(EventString, target); } 15653 15654 override bool cancelable() const { return false; } 15655 } 15656 15657 /// ditto 15658 class FocusOutEvent : Event { 15659 enum EventString = "focusout"; 15660 15661 // FIXME: related target? 15662 this(Widget target) { super(EventString, target); } 15663 15664 override bool cancelable() const { return false; } 15665 } 15666 15667 /// 15668 class ScrollEvent : Event { 15669 enum EventString = "scroll"; 15670 this(Widget target) { super(EventString, target); } 15671 15672 override bool cancelable() const { return false; } 15673 } 15674 15675 /++ 15676 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 15677 15678 History: 15679 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 15680 +/ 15681 class CharEvent : Event { 15682 enum EventString = "char"; 15683 this(Widget target, dchar ch) { 15684 character = ch; 15685 super(EventString, target); 15686 } 15687 15688 immutable dchar character; 15689 } 15690 15691 /++ 15692 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 15693 +/ 15694 abstract class ChangeEventBase : Event { 15695 enum EventString = "change"; 15696 this(Widget target) { 15697 super(EventString, target); 15698 } 15699 15700 /+ 15701 // idk where or how exactly i want to do this. 15702 // i might come back to it later. 15703 15704 // If a widget itself broadcasts one of theses itself, it stops propagation going down 15705 // this way the source doesn't get too confused (think of a nested scroll widget) 15706 // 15707 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 15708 // then you consume that command and change you scroll x position to whatever. then you do 15709 // some kind of change event that is broadcast back to the children and any horizontal scroll 15710 // listeners are now able to update, without having an explicit connection between them. 15711 void broadcastToChildren(string fieldName) { 15712 15713 } 15714 +/ 15715 } 15716 15717 /++ 15718 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. 15719 15720 15721 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). 15722 15723 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);` 15724 15725 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 15726 15727 History: 15728 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. 15729 +/ 15730 class ChangeEvent(T) : ChangeEventBase { 15731 this(Widget target, T delegate() getNewValue) { 15732 assert(getNewValue !is null); 15733 this.getNewValue = getNewValue; 15734 super(target); 15735 } 15736 15737 private T delegate() getNewValue; 15738 15739 /++ 15740 Gets the new value that just changed. 15741 +/ 15742 @property T value() { 15743 return getNewValue(); 15744 } 15745 15746 /// compatibility method for old generic Events 15747 static if(is(immutable T == immutable int)) 15748 override int intValue() { return value; } 15749 /// ditto 15750 static if(is(immutable T == immutable string)) 15751 override string stringValue() { return value; } 15752 } 15753 15754 /++ 15755 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 15756 15757 15758 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 15759 15760 History: 15761 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 15762 +/ 15763 abstract class KeyEventBase : Event { 15764 this(string name, Widget target) { 15765 super(name, target); 15766 } 15767 15768 // for key events 15769 Key key; /// 15770 15771 KeyEvent originalKeyEvent; 15772 15773 /++ 15774 Indicates the current state of the given keyboard modifier keys. 15775 15776 History: 15777 Added to events on April 15, 2020. 15778 +/ 15779 bool ctrlKey; 15780 15781 /// ditto 15782 bool altKey; 15783 15784 /// ditto 15785 bool shiftKey; 15786 15787 /++ 15788 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 15789 15790 See [arsd.simpledisplay.ModifierState] for other possible flags. 15791 +/ 15792 int state; 15793 15794 mixin Register; 15795 } 15796 15797 /++ 15798 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]. 15799 15800 15801 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 15802 15803 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. 15804 15805 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. 15806 15807 See_Also: [KeyUpEvent], [CharEvent] 15808 15809 History: 15810 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 15811 +/ 15812 class KeyDownEvent : KeyEventBase { 15813 enum EventString = "keydown"; 15814 this(Widget target) { super(EventString, target); } 15815 } 15816 15817 /++ 15818 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 15819 15820 15821 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 15822 15823 See_Also: [KeyDownEvent], [CharEvent] 15824 15825 History: 15826 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 15827 +/ 15828 class KeyUpEvent : KeyEventBase { 15829 enum EventString = "keyup"; 15830 this(Widget target) { super(EventString, target); } 15831 } 15832 15833 /++ 15834 Contains shared properties for various mouse events; 15835 15836 15837 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 15838 15839 History: 15840 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 15841 +/ 15842 abstract class MouseEventBase : Event { 15843 this(string name, Widget target) { 15844 super(name, target); 15845 } 15846 15847 // for mouse events 15848 int clientX; /// The mouse event location relative to the target widget 15849 int clientY; /// ditto 15850 15851 int viewportX; /// The mouse event location relative to the window origin 15852 int viewportY; /// ditto 15853 15854 int button; /// See: [MouseEvent.button] 15855 int buttonLinear; /// See: [MouseEvent.buttonLinear] 15856 15857 /++ 15858 Indicates the current state of the given keyboard modifier keys. 15859 15860 History: 15861 Added to mouse events on September 28, 2010. 15862 +/ 15863 bool ctrlKey; 15864 15865 /// ditto 15866 bool altKey; 15867 15868 /// ditto 15869 bool shiftKey; 15870 15871 15872 15873 int state; /// 15874 15875 /++ 15876 for consistent names with key event. 15877 15878 History: 15879 Added September 28, 2021 (dub v10.3) 15880 +/ 15881 alias modifierState = state; 15882 15883 /++ 15884 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 15885 15886 History: 15887 Added May 15, 2021 15888 +/ 15889 bool isMouseWheel() { 15890 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 15891 } 15892 15893 // private 15894 override void adjustClientCoordinates(int deltaX, int deltaY) { 15895 clientX += deltaX; 15896 clientY += deltaY; 15897 } 15898 15899 override void adjustScrolling() { 15900 version(custom_widgets) { // TEMP 15901 viewportX = clientX; 15902 viewportY = clientY; 15903 if(auto se = cast(ScrollableWidget) srcElement) { 15904 clientX += se.scrollOrigin.x; 15905 clientY += se.scrollOrigin.y; 15906 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 15907 //clientX += se.scrollX_; 15908 //clientY += se.scrollY_; 15909 } 15910 } 15911 } 15912 15913 mixin Register; 15914 } 15915 15916 /++ 15917 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 15918 15919 15920 $(WARNING 15921 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 15922 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 15923 behavior. 15924 ) 15925 15926 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 15927 15928 [MouseUpEvent] is sent when the user releases a mouse button. 15929 15930 [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.) 15931 15932 [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. 15933 15934 [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. 15935 15936 [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. 15937 15938 [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. 15939 15940 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 15941 15942 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 15943 15944 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 15945 15946 Rationale: 15947 15948 If you only want to do drag, mousedown/up works just fine being consistently sent. 15949 15950 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). 15951 15952 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. 15953 15954 History: 15955 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. 15956 +/ 15957 class MouseUpEvent : MouseEventBase { 15958 enum EventString = "mouseup"; /// 15959 this(Widget target) { super(EventString, target); } 15960 } 15961 /// ditto 15962 class MouseDownEvent : MouseEventBase { 15963 enum EventString = "mousedown"; /// 15964 this(Widget target) { super(EventString, target); } 15965 } 15966 /// ditto 15967 class MouseMoveEvent : MouseEventBase { 15968 enum EventString = "mousemove"; /// 15969 this(Widget target) { super(EventString, target); } 15970 } 15971 /// ditto 15972 class ClickEvent : MouseEventBase { 15973 enum EventString = "click"; /// 15974 this(Widget target) { super(EventString, target); } 15975 } 15976 /// ditto 15977 class DoubleClickEvent : MouseEventBase { 15978 enum EventString = "dblclick"; /// 15979 this(Widget target) { super(EventString, target); } 15980 } 15981 /// ditto 15982 class MouseOverEvent : Event { 15983 enum EventString = "mouseover"; /// 15984 this(Widget target) { super(EventString, target); } 15985 } 15986 /// ditto 15987 class MouseOutEvent : Event { 15988 enum EventString = "mouseout"; /// 15989 this(Widget target) { super(EventString, target); } 15990 } 15991 /// ditto 15992 class MouseEnterEvent : Event { 15993 enum EventString = "mouseenter"; /// 15994 this(Widget target) { super(EventString, target); } 15995 15996 override bool propagates() const { return false; } 15997 } 15998 /// ditto 15999 class MouseLeaveEvent : Event { 16000 enum EventString = "mouseleave"; /// 16001 this(Widget target) { super(EventString, target); } 16002 16003 override bool propagates() const { return false; } 16004 } 16005 16006 private bool isAParentOf(Widget a, Widget b) { 16007 if(a is null || b is null) 16008 return false; 16009 16010 while(b !is null) { 16011 if(a is b) 16012 return true; 16013 b = b.parent; 16014 } 16015 16016 return false; 16017 } 16018 16019 private struct WidgetAtPointResponse { 16020 Widget widget; 16021 16022 // x, y relative to the widget in the response. 16023 int x; 16024 int y; 16025 } 16026 16027 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 16028 assert(starting !is null); 16029 16030 starting.addScrollPosition(x, y); 16031 16032 auto child = starting.getChildAtPosition(x, y); 16033 while(child) { 16034 if(child.hidden) 16035 continue; 16036 starting = child; 16037 x -= child.x; 16038 y -= child.y; 16039 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 16040 child = r.widget; 16041 if(child is starting) 16042 break; 16043 } 16044 return WidgetAtPointResponse(starting, x, y); 16045 } 16046 16047 version(win32_widgets) { 16048 private: 16049 import core.sys.windows.commctrl; 16050 16051 pragma(lib, "comctl32"); 16052 shared static this() { 16053 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 16054 INITCOMMONCONTROLSEX ic; 16055 ic.dwSize = cast(DWORD) ic.sizeof; 16056 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 16057 if(!InitCommonControlsEx(&ic)) { 16058 //writeln("ICC failed"); 16059 } 16060 } 16061 16062 16063 // everything from here is just win32 headers copy pasta 16064 private: 16065 extern(Windows): 16066 16067 alias HANDLE HMENU; 16068 HMENU CreateMenu(); 16069 bool SetMenu(HWND, HMENU); 16070 HMENU CreatePopupMenu(); 16071 enum MF_POPUP = 0x10; 16072 enum MF_STRING = 0; 16073 16074 16075 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 16076 struct INITCOMMONCONTROLSEX { 16077 DWORD dwSize; 16078 DWORD dwICC; 16079 } 16080 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 16081 enum { 16082 IDB_STD_SMALL_COLOR, 16083 IDB_STD_LARGE_COLOR, 16084 IDB_VIEW_SMALL_COLOR = 4, 16085 IDB_VIEW_LARGE_COLOR = 5 16086 } 16087 enum { 16088 STD_CUT, 16089 STD_COPY, 16090 STD_PASTE, 16091 STD_UNDO, 16092 STD_REDOW, 16093 STD_DELETE, 16094 STD_FILENEW, 16095 STD_FILEOPEN, 16096 STD_FILESAVE, 16097 STD_PRINTPRE, 16098 STD_PROPERTIES, 16099 STD_HELP, 16100 STD_FIND, 16101 STD_REPLACE, 16102 STD_PRINT // = 14 16103 } 16104 16105 alias HANDLE HIMAGELIST; 16106 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 16107 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 16108 BOOL ImageList_Destroy(HIMAGELIST); 16109 16110 uint MAKELONG(ushort a, ushort b) { 16111 return cast(uint) ((b << 16) | a); 16112 } 16113 16114 16115 struct TBBUTTON { 16116 int iBitmap; 16117 int idCommand; 16118 BYTE fsState; 16119 BYTE fsStyle; 16120 version(Win64) 16121 BYTE[6] bReserved; 16122 else 16123 BYTE[2] bReserved; 16124 DWORD dwData; 16125 INT_PTR iString; 16126 } 16127 16128 enum { 16129 TB_ADDBUTTONSA = WM_USER + 20, 16130 TB_INSERTBUTTONA = WM_USER + 21, 16131 TB_GETIDEALSIZE = WM_USER + 99, 16132 } 16133 16134 struct SIZE { 16135 LONG cx; 16136 LONG cy; 16137 } 16138 16139 16140 enum { 16141 TBSTATE_CHECKED = 1, 16142 TBSTATE_PRESSED = 2, 16143 TBSTATE_ENABLED = 4, 16144 TBSTATE_HIDDEN = 8, 16145 TBSTATE_INDETERMINATE = 16, 16146 TBSTATE_WRAP = 32 16147 } 16148 16149 16150 16151 enum { 16152 ILC_COLOR = 0, 16153 ILC_COLOR4 = 4, 16154 ILC_COLOR8 = 8, 16155 ILC_COLOR16 = 16, 16156 ILC_COLOR24 = 24, 16157 ILC_COLOR32 = 32, 16158 ILC_COLORDDB = 254, 16159 ILC_MASK = 1, 16160 ILC_PALETTE = 2048 16161 } 16162 16163 16164 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 16165 16166 16167 enum { 16168 TB_ENABLEBUTTON = WM_USER + 1, 16169 TB_CHECKBUTTON, 16170 TB_PRESSBUTTON, 16171 TB_HIDEBUTTON, 16172 TB_INDETERMINATE, // = WM_USER + 5, 16173 TB_ISBUTTONENABLED = WM_USER + 9, 16174 TB_ISBUTTONCHECKED, 16175 TB_ISBUTTONPRESSED, 16176 TB_ISBUTTONHIDDEN, 16177 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 16178 TB_SETSTATE = WM_USER + 17, 16179 TB_GETSTATE = WM_USER + 18, 16180 TB_ADDBITMAP = WM_USER + 19, 16181 TB_DELETEBUTTON = WM_USER + 22, 16182 TB_GETBUTTON, 16183 TB_BUTTONCOUNT, 16184 TB_COMMANDTOINDEX, 16185 TB_SAVERESTOREA, 16186 TB_CUSTOMIZE, 16187 TB_ADDSTRINGA, 16188 TB_GETITEMRECT, 16189 TB_BUTTONSTRUCTSIZE, 16190 TB_SETBUTTONSIZE, 16191 TB_SETBITMAPSIZE, 16192 TB_AUTOSIZE, // = WM_USER + 33, 16193 TB_GETTOOLTIPS = WM_USER + 35, 16194 TB_SETTOOLTIPS = WM_USER + 36, 16195 TB_SETPARENT = WM_USER + 37, 16196 TB_SETROWS = WM_USER + 39, 16197 TB_GETROWS, 16198 TB_GETBITMAPFLAGS, 16199 TB_SETCMDID, 16200 TB_CHANGEBITMAP, 16201 TB_GETBITMAP, 16202 TB_GETBUTTONTEXTA, 16203 TB_REPLACEBITMAP, // = WM_USER + 46, 16204 TB_GETBUTTONSIZE = WM_USER + 58, 16205 TB_SETBUTTONWIDTH = WM_USER + 59, 16206 TB_GETBUTTONTEXTW = WM_USER + 75, 16207 TB_SAVERESTOREW = WM_USER + 76, 16208 TB_ADDSTRINGW = WM_USER + 77, 16209 } 16210 16211 extern(Windows) 16212 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 16213 16214 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 16215 16216 16217 enum { 16218 TB_SETINDENT = WM_USER + 47, 16219 TB_SETIMAGELIST, 16220 TB_GETIMAGELIST, 16221 TB_LOADIMAGES, 16222 TB_GETRECT, 16223 TB_SETHOTIMAGELIST, 16224 TB_GETHOTIMAGELIST, 16225 TB_SETDISABLEDIMAGELIST, 16226 TB_GETDISABLEDIMAGELIST, 16227 TB_SETSTYLE, 16228 TB_GETSTYLE, 16229 //TB_GETBUTTONSIZE, 16230 //TB_SETBUTTONWIDTH, 16231 TB_SETMAXTEXTROWS, 16232 TB_GETTEXTROWS // = WM_USER + 61 16233 } 16234 16235 enum { 16236 CCM_FIRST = 0x2000, 16237 CCM_LAST = CCM_FIRST + 0x200, 16238 CCM_SETBKCOLOR = 8193, 16239 CCM_SETCOLORSCHEME = 8194, 16240 CCM_GETCOLORSCHEME = 8195, 16241 CCM_GETDROPTARGET = 8196, 16242 CCM_SETUNICODEFORMAT = 8197, 16243 CCM_GETUNICODEFORMAT = 8198, 16244 CCM_SETVERSION = 0x2007, 16245 CCM_GETVERSION = 0x2008, 16246 CCM_SETNOTIFYWINDOW = 0x2009 16247 } 16248 16249 16250 enum { 16251 PBM_SETRANGE = WM_USER + 1, 16252 PBM_SETPOS, 16253 PBM_DELTAPOS, 16254 PBM_SETSTEP, 16255 PBM_STEPIT, // = WM_USER + 5 16256 PBM_SETRANGE32 = 1030, 16257 PBM_GETRANGE, 16258 PBM_GETPOS, 16259 PBM_SETBARCOLOR, // = 1033 16260 PBM_SETBKCOLOR = CCM_SETBKCOLOR 16261 } 16262 16263 enum { 16264 PBS_SMOOTH = 1, 16265 PBS_VERTICAL = 4 16266 } 16267 16268 enum { 16269 ICC_LISTVIEW_CLASSES = 1, 16270 ICC_TREEVIEW_CLASSES = 2, 16271 ICC_BAR_CLASSES = 4, 16272 ICC_TAB_CLASSES = 8, 16273 ICC_UPDOWN_CLASS = 16, 16274 ICC_PROGRESS_CLASS = 32, 16275 ICC_HOTKEY_CLASS = 64, 16276 ICC_ANIMATE_CLASS = 128, 16277 ICC_WIN95_CLASSES = 255, 16278 ICC_DATE_CLASSES = 256, 16279 ICC_USEREX_CLASSES = 512, 16280 ICC_COOL_CLASSES = 1024, 16281 ICC_STANDARD_CLASSES = 0x00004000, 16282 } 16283 16284 enum WM_USER = 1024; 16285 } 16286 16287 version(win32_widgets) 16288 pragma(lib, "comdlg32"); 16289 16290 16291 /// 16292 enum GenericIcons : ushort { 16293 None, /// 16294 // these happen to match the win32 std icons numerically if you just subtract one from the value 16295 Cut, /// 16296 Copy, /// 16297 Paste, /// 16298 Undo, /// 16299 Redo, /// 16300 Delete, /// 16301 New, /// 16302 Open, /// 16303 Save, /// 16304 PrintPreview, /// 16305 Properties, /// 16306 Help, /// 16307 Find, /// 16308 Replace, /// 16309 Print, /// 16310 } 16311 16312 enum FileDialogType { 16313 Automatic, 16314 Open, 16315 Save 16316 } 16317 16318 /++ 16319 The default string [FileName] refers to to store the last file referenced. You can use this if you like, or provide a different variable to `FileName` in your function. 16320 +/ 16321 string previousFileReferenced; 16322 16323 /++ 16324 Used in automatic menu functions to indicate that the user should be able to browse for a file. 16325 16326 Params: 16327 storage = an alias to a `static string` variable that stores the last file referenced. It will 16328 use this to pre-fill the dialog with a suggestion. 16329 16330 Please note that it MUST be `static` or you will get compile errors. 16331 16332 filters = the filters param to [getFileName] 16333 16334 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 16335 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 16336 a save dialog box. Otherwise, it will show an open dialog box. 16337 +/ 16338 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 16339 string name; 16340 alias name this; 16341 16342 @implicit this(string name) { 16343 this.name = name; 16344 } 16345 } 16346 16347 /++ 16348 Gets a file name for an open or save operation, calling your `onOK` function when the user has selected one. This function may or may not block depending on the operating system, you MUST assume it will complete asynchronously. 16349 16350 History: 16351 onCancel was added November 6, 2021. 16352 16353 The dialog itself on Linux was modified on December 2, 2021 to include 16354 a directory picker in addition to the command line completion view. 16355 16356 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 16357 16358 The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version. 16359 Future_directions: 16360 I want to add some kind of custom preview and maybe thumbnail thing in the future, 16361 at least on Linux, maybe on Windows too. 16362 +/ 16363 void getOpenFileName( 16364 Window owner, 16365 void delegate(string) onOK, 16366 string prefilledName = null, 16367 string[] filters = null, 16368 void delegate() onCancel = null, 16369 string initialDirectory = null, 16370 ) 16371 { 16372 return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory); 16373 } 16374 16375 /// ditto 16376 void getSaveFileName( 16377 Window owner, 16378 void delegate(string) onOK, 16379 string prefilledName = null, 16380 string[] filters = null, 16381 void delegate() onCancel = null, 16382 string initialDirectory = null, 16383 ) 16384 { 16385 return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory); 16386 } 16387 16388 // deprecated("Pass an explicit owner window as the first argument, even if `null`. You can usually pass the `parentWindow` member of the widget that prompted this interaction.") 16389 /// ditto 16390 void getOpenFileName( 16391 void delegate(string) onOK, 16392 string prefilledName = null, 16393 string[] filters = null, 16394 void delegate() onCancel = null, 16395 string initialDirectory = null, 16396 ) 16397 { 16398 return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory); 16399 } 16400 16401 /// ditto 16402 void getSaveFileName( 16403 void delegate(string) onOK, 16404 string prefilledName = null, 16405 string[] filters = null, 16406 void delegate() onCancel = null, 16407 string initialDirectory = null, 16408 ) 16409 { 16410 return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory); 16411 } 16412 16413 /++ 16414 It is possible to override or customize the file dialog in some cases. These members provide those hooks: you do `fileDialogDelegate = new YourSubclassOf_FileDialogDelegate;` and you can do your own thing. 16415 16416 This is a customization hook and you should not call methods on this class directly. Use the public functions [getOpenFileName] and [getSaveFileName], or make an automatic dialog with [FileName] instead. 16417 16418 History: 16419 Added January 1, 2025 16420 +/ 16421 class FileDialogDelegate { 16422 16423 /++ 16424 16425 +/ 16426 static abstract class PreviewWidget : Widget { 16427 /// Call this from your subclass' constructor 16428 this(Widget parent) { 16429 super(parent); 16430 } 16431 16432 /// Load the file given to you and show its preview inside the widget here 16433 abstract void previewFile(string filename); 16434 } 16435 16436 /++ 16437 Override this to add preview capabilities to the dialog for certain files. 16438 +/ 16439 protected PreviewWidget makePreviewWidget(Widget parent) { 16440 return null; 16441 } 16442 16443 /++ 16444 Override this to change the dialog entirely. 16445 16446 This function IS allowed to block, but is NOT required to. 16447 +/ 16448 protected void getFileName( 16449 Window owner, 16450 bool openOrSave, // true if open, false if save 16451 void delegate(string) onOK, 16452 string prefilledName, 16453 string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 16454 void delegate() onCancel, 16455 string initialDirectory, 16456 ) 16457 { 16458 16459 version(win32_widgets) { 16460 import core.sys.windows.commdlg; 16461 /* 16462 Ofn.lStructSize = sizeof(OPENFILENAME); 16463 Ofn.hwndOwner = hWnd; 16464 Ofn.lpstrFilter = szFilter; 16465 Ofn.lpstrFile= szFile; 16466 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 16467 Ofn.lpstrFileTitle = szFileTitle; 16468 Ofn.nMaxFileTitle = sizeof(szFileTitle); 16469 Ofn.lpstrInitialDir = (LPSTR)NULL; 16470 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 16471 Ofn.lpstrTitle = szTitle; 16472 */ 16473 16474 16475 wchar[1024] file = 0; 16476 wchar[1024] filterBuffer = 0; 16477 makeWindowsString(prefilledName, file[]); 16478 OPENFILENAME ofn; 16479 ofn.lStructSize = ofn.sizeof; 16480 ofn.hwndOwner = owner is null ? null : owner.win.hwnd; 16481 if(filters.length) { 16482 string filter; 16483 foreach(i, f; filters) { 16484 filter ~= f; 16485 filter ~= "\0"; 16486 } 16487 filter ~= "\0"; 16488 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 16489 } 16490 ofn.lpstrFile = file.ptr; 16491 ofn.nMaxFile = file.length; 16492 16493 wchar[1024] initialDir = 0; 16494 if(initialDirectory !is null) { 16495 makeWindowsString(initialDirectory, initialDir[]); 16496 ofn.lpstrInitialDir = file.ptr; 16497 } 16498 16499 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 16500 { 16501 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 16502 if(okString.length && okString[$-1] == '\0') 16503 okString = okString[0..$-1]; 16504 onOK(okString); 16505 } else { 16506 if(onCancel) 16507 onCancel(); 16508 } 16509 } else version(custom_widgets) { 16510 filters ~= ["All Files\0*.*"]; 16511 auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner); 16512 picker.onOK = onOK; 16513 picker.onCancel = onCancel; 16514 picker.show(); 16515 } 16516 } 16517 16518 } 16519 16520 /// ditto 16521 FileDialogDelegate fileDialogDelegate() { 16522 if(fileDialogDelegate_ is null) 16523 fileDialogDelegate_ = new FileDialogDelegate(); 16524 return fileDialogDelegate_; 16525 } 16526 16527 /// ditto 16528 void fileDialogDelegate(FileDialogDelegate replacement) { 16529 fileDialogDelegate_ = replacement; 16530 } 16531 16532 private FileDialogDelegate fileDialogDelegate_; 16533 16534 struct FileNameFilter { 16535 string description; 16536 string[] globPatterns; 16537 16538 string toString() { 16539 string ret; 16540 ret ~= description; 16541 ret ~= " ("; 16542 foreach(idx, pattern; globPatterns) { 16543 if(idx) 16544 ret ~= "; "; 16545 ret ~= pattern; 16546 } 16547 ret ~= ")"; 16548 16549 return ret; 16550 } 16551 16552 static FileNameFilter fromString(string s) { 16553 size_t end = s.length; 16554 size_t start = 0; 16555 foreach_reverse(idx, ch; s) { 16556 if(ch == ')' && end == s.length) 16557 end = idx; 16558 else if(ch == '(' && end != s.length) { 16559 start = idx + 1; 16560 break; 16561 } 16562 } 16563 16564 FileNameFilter fnf; 16565 fnf.description = s[0 .. start ? start - 1 : 0]; 16566 size_t globStart = 0; 16567 s = s[start .. end]; 16568 foreach(idx, ch; s) 16569 if(ch == ';') { 16570 auto ptn = stripInternal(s[globStart .. idx]); 16571 if(ptn.length) 16572 fnf.globPatterns ~= ptn; 16573 globStart = idx + 1; 16574 16575 } 16576 auto ptn = stripInternal(s[globStart .. $]); 16577 if(ptn.length) 16578 fnf.globPatterns ~= ptn; 16579 return fnf; 16580 } 16581 } 16582 16583 struct FileNameFilterSet { 16584 FileNameFilter[] filters; 16585 16586 static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) { 16587 FileNameFilter[] ret; 16588 16589 foreach(filter; filters) { 16590 FileNameFilter fnf; 16591 size_t filterStartPoint; 16592 foreach(idx, ch; filter) { 16593 if(ch == 0) { 16594 fnf.description = filter[0 .. idx]; 16595 filterStartPoint = idx + 1; 16596 } else if(filterStartPoint && ch == ';') { 16597 fnf.globPatterns ~= filter[filterStartPoint .. idx]; 16598 filterStartPoint = idx + 1; 16599 } 16600 } 16601 fnf.globPatterns ~= filter[filterStartPoint .. $]; 16602 16603 ret ~= fnf; 16604 } 16605 16606 return FileNameFilterSet(ret); 16607 } 16608 } 16609 16610 void getFileName( 16611 Window owner, 16612 bool openOrSave, 16613 void delegate(string) onOK, 16614 string prefilledName = null, 16615 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 16616 void delegate() onCancel = null, 16617 string initialDirectory = null, 16618 ) 16619 { 16620 return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory); 16621 } 16622 16623 version(custom_widgets) 16624 private 16625 class FilePicker : Dialog { 16626 void delegate(string) onOK; 16627 void delegate() onCancel; 16628 LabeledLineEdit lineEdit; 16629 bool isOpenDialogInsteadOfSave; 16630 16631 static struct HistoryItem { 16632 string cwd; 16633 FileNameFilter filters; 16634 } 16635 HistoryItem[] historyStack; 16636 size_t historyStackPosition; 16637 16638 void back() { 16639 if(historyStackPosition) { 16640 historyStackPosition--; 16641 currentDirectory = historyStack[historyStackPosition].cwd; 16642 currentFilter = historyStack[historyStackPosition].filters; 16643 filesOfType.content = currentFilter.toString(); 16644 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 16645 lineEdit.focus(); 16646 } 16647 } 16648 16649 void forward() { 16650 if(historyStackPosition + 1 < historyStack.length) { 16651 historyStackPosition++; 16652 currentDirectory = historyStack[historyStackPosition].cwd; 16653 currentFilter = historyStack[historyStackPosition].filters; 16654 filesOfType.content = currentFilter.toString(); 16655 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 16656 lineEdit.focus(); 16657 } 16658 } 16659 16660 void up() { 16661 currentDirectory = currentDirectory ~ ".."; 16662 loadFiles(currentDirectory, currentFilter); 16663 lineEdit.focus(); 16664 } 16665 16666 void refresh() { 16667 loadFiles(currentDirectory, currentFilter); 16668 lineEdit.focus(); 16669 } 16670 16671 // returns common prefix 16672 static struct CommonPrefixInfo { 16673 string commonPrefix; 16674 int fileCount; 16675 string exactMatch; 16676 } 16677 CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) { 16678 16679 if(!comingFromHistory) { 16680 if(historyStack.length) { 16681 historyStack = historyStack[0 .. historyStackPosition + 1]; 16682 historyStack.assumeSafeAppend(); 16683 } 16684 historyStack ~= HistoryItem(cwd, filters); 16685 historyStackPosition = historyStack.length - 1; 16686 } 16687 16688 string[] files; 16689 string[] dirs; 16690 16691 dirs ~= "$HOME"; 16692 dirs ~= "$PWD"; 16693 16694 string commonPrefix; 16695 int commonPrefixCount; 16696 string exactMatch; 16697 16698 bool matchesFilter(string name) { 16699 foreach(filter; filters.globPatterns) { 16700 if( 16701 filter.length <= 1 || 16702 filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .* 16703 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 16704 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 16705 ) 16706 { 16707 if(name.length > 1 && name[0] == '.') 16708 if(filter.length == 0 || filter[0] != '.') 16709 return false; 16710 16711 return true; 16712 } 16713 } 16714 16715 return false; 16716 } 16717 16718 void considerCommonPrefix(string name, bool prefiltered) { 16719 if(!prefiltered && !matchesFilter(name)) 16720 return; 16721 16722 if(commonPrefix is null) { 16723 commonPrefix = name; 16724 commonPrefixCount = 1; 16725 exactMatch = commonPrefix; 16726 } else { 16727 foreach(idx, char i; name) { 16728 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 16729 commonPrefix = commonPrefix[0 .. idx]; 16730 commonPrefixCount ++; 16731 exactMatch = null; 16732 break; 16733 } 16734 } 16735 } 16736 } 16737 16738 bool applyFilterToDirectories = true; 16739 bool showDotFiles = false; 16740 foreach(filter; filters.globPatterns) { 16741 if(filter == ".*") 16742 showDotFiles = true; 16743 else foreach(ch; filter) 16744 if(ch == '.') { 16745 // a filter like *.exe should not apply to the directory 16746 applyFilterToDirectories = false; 16747 break; 16748 } 16749 } 16750 16751 try 16752 getFiles(cwd, (string name, bool isDirectory) { 16753 if(name == ".") 16754 return; // skip this as unnecessary 16755 if(isDirectory) { 16756 if(applyFilterToDirectories) { 16757 if(matchesFilter(name)) { 16758 dirs ~= name; 16759 considerCommonPrefix(name, false); 16760 } 16761 } else if(name != ".." && name.length > 1 && name[0] == '.') { 16762 if(showDotFiles) { 16763 dirs ~= name; 16764 considerCommonPrefix(name, false); 16765 } 16766 } else { 16767 dirs ~= name; 16768 considerCommonPrefix(name, false); 16769 } 16770 } else { 16771 if(matchesFilter(name)) { 16772 files ~= name; 16773 16774 //if(filter.length > 0 && filter[$-1] == '*') { 16775 considerCommonPrefix(name, true); 16776 //} 16777 } 16778 } 16779 }); 16780 catch(ArsdExceptionBase e) { 16781 messageBox("Unable to read requested directory"); 16782 // FIXME: give them a chance to create it? or at least go back? 16783 /+ 16784 comingFromHistory = true; 16785 back(); 16786 return null; 16787 +/ 16788 } 16789 16790 extern(C) static int comparator(scope const void* a, scope const void* b) { 16791 // FIXME: make it a natural sort for numbers 16792 // maybe put dot files at the end too. 16793 auto sa = *cast(string*) a; 16794 auto sb = *cast(string*) b; 16795 16796 for(int i = 0; i < sa.length; i++) { 16797 if(i == sb.length) 16798 return 1; 16799 auto diff = sa[i] - sb[i]; 16800 if(diff) 16801 return diff; 16802 } 16803 16804 return 0; 16805 } 16806 16807 nonPhobosSort(files, &comparator); 16808 nonPhobosSort(dirs, &comparator); 16809 16810 listWidget.clear(); 16811 dirWidget.clear(); 16812 foreach(name; dirs) 16813 dirWidget.addOption(name); 16814 foreach(name; files) 16815 listWidget.addOption(name); 16816 16817 return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch); 16818 } 16819 16820 ListWidget listWidget; 16821 ListWidget dirWidget; 16822 16823 FreeEntrySelection filesOfType; 16824 LineEdit directoryHolder; 16825 16826 string currentDirectory_; 16827 FileNameFilter currentNonTabFilter; 16828 FileNameFilter currentFilter; 16829 FileNameFilterSet filterOptions; 16830 16831 void currentDirectory(string s) { 16832 currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString(); 16833 directoryHolder.content = currentDirectory_; 16834 } 16835 string currentDirectory() { 16836 return currentDirectory_; 16837 } 16838 16839 private string getUserHomeDir() { 16840 import core.stdc.stdlib; 16841 version(Windows) 16842 return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup; 16843 else 16844 return (stringz(getenv("HOME")).borrow).idup; 16845 } 16846 16847 private string expandTilde(string s) { 16848 // FIXME: cannot look up other user dirs 16849 if(s.length == 1 && s == "~") 16850 return getUserHomeDir(); 16851 if(s.length > 1 && s[0] == '~' && s[1] == '/') 16852 return getUserHomeDir() ~ s[1 .. $]; 16853 return s; 16854 } 16855 16856 // FIXME: allow many files to be picked too sometimes 16857 16858 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 16859 this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) { 16860 this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat); 16861 this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave; 16862 super(owner, 500, 400, "Choose File..."); // owner); 16863 16864 { 16865 auto navbar = new HorizontalLayout(24, this); 16866 auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar); 16867 auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar); 16868 auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button 16869 16870 directoryHolder = new LineEdit(navbar); 16871 16872 directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) { 16873 if(kde.key == Key.Enter || kde.key == Key.PadEnter) { 16874 kde.stopPropagation(); 16875 16876 currentDirectory = directoryHolder.content; 16877 loadFiles(currentDirectory, currentFilter); 16878 16879 lineEdit.focus(); 16880 } 16881 }); 16882 16883 auto refreshButton = new ToolButton(new Action("R", 0, &this.refresh), navbar); // can live without refresh since you can cancel and reopen but still nice. it should be automatic when it can maybe. 16884 16885 /+ 16886 auto newDirectoryButton = new ToolButton(new Action("N"), navbar); 16887 16888 // FIXME: make sure putting `.` in the dir filter goes back to the CWD 16889 // and that ~ goes back to the home dir 16890 // and blanking it goes back to the suggested dir 16891 16892 auto homeButton = new ToolButton(new Action("H"), navbar); 16893 auto cwdButton = new ToolButton(new Action("."), navbar); 16894 auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar); 16895 +/ 16896 16897 filesOfType = new class FreeEntrySelection { 16898 this() { 16899 string[] opt; 16900 foreach(option; filterOptions.filters) 16901 opt ~= option.toString; 16902 super(opt, navbar); 16903 } 16904 override int flexBasisWidth() { 16905 return scaleWithDpi(150); 16906 } 16907 override int widthStretchiness() { 16908 return 1;//super.widthStretchiness() / 2; 16909 } 16910 }; 16911 filesOfType.setSelection(0); 16912 currentFilter = filterOptions.filters[0]; 16913 currentNonTabFilter = currentFilter; 16914 } 16915 16916 { 16917 auto mainGrid = new GridLayout(4, 1, this); 16918 16919 dirWidget = new ListWidget(mainGrid); 16920 listWidget = new ListWidget(mainGrid); 16921 listWidget.tabStop = false; 16922 dirWidget.tabStop = false; 16923 16924 FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid); 16925 16926 mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1); 16927 mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1); 16928 if(previewWidget) 16929 mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1); 16930 16931 // double click events normally trigger something else but 16932 // here user might be clicking kinda fast and we'd rather just 16933 // keep it 16934 dirWidget.addEventListener((scope DoubleClickEvent dev) { 16935 auto ce = new ChangeEvent!void(dirWidget, () {}); 16936 ce.dispatch(); 16937 lineEdit.focus(); 16938 }); 16939 16940 dirWidget.addEventListener((scope ChangeEvent!void sce) { 16941 string v; 16942 foreach(o; dirWidget.options) 16943 if(o.selected) { 16944 v = o.label; 16945 break; 16946 } 16947 if(v.length) { 16948 if(v == "$HOME") 16949 currentDirectory = getUserHomeDir(); 16950 else if(v == "$PWD") 16951 currentDirectory = "."; 16952 else 16953 currentDirectory = currentDirectory ~ "/" ~ v; 16954 loadFiles(currentDirectory, currentFilter); 16955 } 16956 16957 dirWidget.focusOn = -1; 16958 lineEdit.focus(); 16959 }); 16960 16961 // double click here, on the other hand, selects the file 16962 // and moves on 16963 listWidget.addEventListener((scope DoubleClickEvent dev) { 16964 OK(); 16965 }); 16966 } 16967 16968 lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this); 16969 lineEdit.focus(); 16970 lineEdit.addEventListener(delegate(CharEvent event) { 16971 if(event.character == '\t' || event.character == '\n') 16972 event.preventDefault(); 16973 }); 16974 16975 listWidget.addEventListener(EventType.change, () { 16976 foreach(o; listWidget.options) 16977 if(o.selected) 16978 lineEdit.content = o.label; 16979 }); 16980 16981 currentDirectory = initialDirectory is null ? "." : initialDirectory; 16982 loadFiles(currentDirectory, currentFilter); 16983 16984 filesOfType.addEventListener(delegate (ChangeEvent!string ce) { 16985 currentFilter = FileNameFilter.fromString(ce.stringValue); 16986 currentNonTabFilter = currentFilter; 16987 loadFiles(currentDirectory, currentFilter); 16988 // lineEdit.focus(); // this causes a recursive crash..... 16989 }); 16990 16991 lineEdit.addEventListener((KeyDownEvent event) { 16992 if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) { 16993 16994 auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory)); 16995 currentDirectory = path.directoryName; 16996 auto current = path.filename; 16997 16998 auto newFilter = current; 16999 if(current.length && current[0] != '*' && current[$-1] != '*') 17000 newFilter ~= "*"; 17001 else if(newFilter.length == 0) 17002 newFilter = "*"; 17003 17004 auto newFilterObj = FileNameFilter("Custom filter", [newFilter]); 17005 17006 CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj); 17007 if(commonPrefix.fileCount == 1) { 17008 // exactly one file, let's see what it is 17009 auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory)); 17010 if(getFileType(specificFile.toString) == FileType.dir) { 17011 // a directory means we should change to it and keep the old filter 17012 currentDirectory = specificFile.toString(); 17013 lineEdit.content = specificFile.toString() ~ "/"; 17014 loadFiles(currentDirectory, currentFilter); 17015 } else { 17016 // any other file should be selected in the list 17017 currentDirectory = specificFile.directoryName; 17018 current = specificFile.filename; 17019 lineEdit.content = current; 17020 loadFiles(currentDirectory, currentFilter); 17021 } 17022 } else if(commonPrefix.fileCount > 1) { 17023 currentFilter = newFilterObj; 17024 filesOfType.content = currentFilter.toString(); 17025 lineEdit.content = commonPrefix.commonPrefix; 17026 } else { 17027 // if there were no files, we don't really want to change the filter.. 17028 sdpyPrintDebugString("no files"); 17029 } 17030 17031 // FIXME: if that is a directory, add the slash? or even go inside? 17032 17033 event.preventDefault(); 17034 } 17035 }); 17036 17037 lineEdit.content = expandTilde(prefilledName); 17038 17039 auto hl = new HorizontalLayout(60, this); 17040 auto cancelButton = new Button("Cancel", hl); 17041 auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl); 17042 17043 cancelButton.addEventListener(EventType.triggered, &Cancel); 17044 okButton.addEventListener(EventType.triggered, &OK); 17045 17046 this.addEventListener((KeyDownEvent event) { 17047 if(event.key == Key.Enter || event.key == Key.PadEnter) { 17048 event.preventDefault(); 17049 OK(); 17050 } 17051 else if(event.key == Key.Escape) 17052 Cancel(); 17053 else if(event.key == Key.F5) 17054 refresh(); 17055 else if(event.key == Key.Up && event.altKey) 17056 up(); // ditto 17057 else if(event.key == Key.Left && event.altKey) 17058 back(); // FIXME: it sends the key to the line edit too 17059 else if(event.key == Key.Right && event.altKey) 17060 forward(); // ditto 17061 else if(event.key == Key.Up) 17062 listWidget.setSelection(listWidget.getSelection() - 1); 17063 else if(event.key == Key.Down) 17064 listWidget.setSelection(listWidget.getSelection() + 1); 17065 }); 17066 17067 // FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted 17068 // FIXME: button to create new directory 17069 // FIXME: show dirs in the files list too? idk. 17070 17071 // FIXME: support ~ as alias for home in the input 17072 // FIXME: tab complete ought to be able to change+complete dir too 17073 } 17074 17075 override void OK() { 17076 if(lineEdit.content.length) { 17077 auto c = expandTilde(lineEdit.content); 17078 17079 FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory)); 17080 17081 auto ft = getFileType(accepted.toString); 17082 17083 if(ft == FileType.error && isOpenDialogInsteadOfSave) { 17084 // FIXME: tell the user why 17085 messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel."); 17086 lineEdit.focus(); 17087 return; 17088 17089 } 17090 17091 // FIXME: symlinks to dirs should prolly also get this behavior 17092 if(ft == FileType.dir) { 17093 currentDirectory = accepted.toString; 17094 17095 currentFilter = currentNonTabFilter; 17096 filesOfType.content = currentFilter.toString(); 17097 17098 loadFiles(currentDirectory, currentFilter); 17099 lineEdit.content = ""; 17100 17101 lineEdit.focus(); 17102 17103 return; 17104 } 17105 17106 if(onOK) 17107 onOK(accepted.toString); 17108 } 17109 close(); 17110 } 17111 17112 override void Cancel() { 17113 if(onCancel) 17114 onCancel(); 17115 close(); 17116 } 17117 } 17118 17119 private enum FileType { 17120 error, 17121 dir, 17122 other 17123 } 17124 17125 private FileType getFileType(string name) { 17126 version(Windows) { 17127 auto ws = WCharzBuffer(name); 17128 auto ret = GetFileAttributesW(ws.ptr); 17129 if(ret == INVALID_FILE_ATTRIBUTES) 17130 return FileType.error; 17131 return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other; 17132 } else version(Posix) { 17133 import core.sys.posix.sys.stat; 17134 stat_t buf; 17135 auto ret = stat((name ~ '\0').ptr, &buf); 17136 if(ret == -1) 17137 return FileType.error; 17138 return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other; 17139 } else assert(0, "Not implemented"); 17140 } 17141 17142 /* 17143 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 17144 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 17145 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 17146 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 17147 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 17148 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 17149 http://www.sbin.org/doc/Xlib/chapt_03.html 17150 17151 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 17152 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 17153 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 17154 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 17155 */ 17156 17157 17158 // These are all for setMenuAndToolbarFromAnnotatedCode 17159 /// This item in the menu will be preceded by a separator line 17160 /// Group: generating_from_code 17161 struct separator {} 17162 deprecated("It was misspelled, use separator instead") alias seperator = separator; 17163 /// Program-wide keyboard shortcut to trigger the action 17164 /// Group: generating_from_code 17165 struct accelerator { string keyString; } 17166 /// tells which menu the action will be on 17167 /// Group: generating_from_code 17168 struct menu { string name; } 17169 /// Describes which toolbar section the action appears on 17170 /// Group: generating_from_code 17171 struct toolbar { string groupName; } 17172 /// 17173 /// Group: generating_from_code 17174 struct icon { ushort id; } 17175 /// 17176 /// Group: generating_from_code 17177 struct label { string label; } 17178 /// 17179 /// Group: generating_from_code 17180 struct hotkey { dchar ch; } 17181 /// 17182 /// Group: generating_from_code 17183 struct tip { string tip; } 17184 /// 17185 /// Group: generating_from_code 17186 enum context_menu = menu.init; 17187 17188 17189 /++ 17190 Observes and allows inspection of an object via automatic gui 17191 +/ 17192 /// Group: generating_from_code 17193 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 17194 return new ObjectInspectionWindowImpl!(T)(t); 17195 } 17196 17197 class ObjectInspectionWindow : Window { 17198 this(int a, int b, string c) { 17199 super(a, b, c); 17200 } 17201 17202 abstract void readUpdatesFromObject(); 17203 } 17204 17205 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 17206 T t; 17207 this(T t) { 17208 this.t = t; 17209 17210 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 17211 17212 foreach(memberName; __traits(derivedMembers, T)) {{ 17213 alias member = I!(__traits(getMember, t, memberName))[0]; 17214 alias type = typeof(member); 17215 static if(is(type == int)) { 17216 auto le = new LabeledLineEdit(memberName ~ ": ", this); 17217 //le.addEventListener("char", (Event ev) { 17218 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 17219 //ev.preventDefault(); 17220 //}); 17221 le.addEventListener(EventType.change, (Event ev) { 17222 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 17223 }); 17224 17225 updateMemberDelegates[memberName] = () { 17226 le.content = toInternal!string(__traits(getMember, t, memberName)); 17227 }; 17228 } 17229 }} 17230 } 17231 17232 void delegate()[string] updateMemberDelegates; 17233 17234 override void readUpdatesFromObject() { 17235 foreach(k, v; updateMemberDelegates) 17236 v(); 17237 } 17238 } 17239 17240 /++ 17241 Creates a dialog based on a data structure. 17242 17243 --- 17244 dialog(window, (YourStructure value) { 17245 // the user filled in the struct and clicked OK, 17246 // you can check the members now 17247 }); 17248 --- 17249 17250 Params: 17251 initialData = the initial value to show in the dialog. It will not modify this unless 17252 it is a class then it might, no promises. 17253 17254 History: 17255 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 17256 17257 The overloads with `parent` were added September 29, 2024. The ones without it are likely to 17258 be deprecated soon. 17259 +/ 17260 /// Group: generating_from_code 17261 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17262 dialog(null, T.init, onOK, onCancel, title); 17263 } 17264 /// ditto 17265 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17266 dialog(null, T.init, onOK, onCancel, title); 17267 } 17268 /// ditto 17269 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17270 dialog(parent, T.init, onOK, onCancel, title); 17271 } 17272 /// ditto 17273 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17274 dialog(parent, initialData, onOK, onCancel, title); 17275 } 17276 /// ditto 17277 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17278 auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title); 17279 dg.show(); 17280 } 17281 17282 private static template I(T...) { alias I = T; } 17283 17284 17285 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 17286 if(name == "id") 17287 return allLowerCase ? name : "ID"; 17288 17289 char[160] buffer; 17290 int bufferIndex = 0; 17291 bool shouldCap = true; 17292 bool shouldSpace; 17293 bool lastWasCap; 17294 foreach(idx, char ch; name) { 17295 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 17296 17297 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 17298 if(lastWasCap) { 17299 // two caps in a row, don't change. Prolly acronym. 17300 } else { 17301 if(idx) 17302 shouldSpace = true; // new word, add space 17303 } 17304 17305 lastWasCap = true; 17306 } else { 17307 lastWasCap = false; 17308 } 17309 17310 if(shouldSpace) { 17311 buffer[bufferIndex++] = space; 17312 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 17313 shouldSpace = false; 17314 } 17315 if(shouldCap) { 17316 if(ch >= 'a' && ch <= 'z') 17317 ch -= 32; 17318 shouldCap = false; 17319 } 17320 if(allLowerCase && ch >= 'A' && ch <= 'Z') 17321 ch += 32; 17322 buffer[bufferIndex++] = ch; 17323 } 17324 return buffer[0 .. bufferIndex].idup; 17325 } 17326 17327 /++ 17328 This is the implementation for [dialog]. None of its details are guaranteed stable and may change at any time; the stable interface is just the [dialog] function at this time. 17329 +/ 17330 class AutomaticDialog(T) : Dialog { 17331 T t; 17332 17333 void delegate(T) onOK; 17334 void delegate() onCancel; 17335 17336 override int paddingTop() { return defaultLineHeight; } 17337 override int paddingBottom() { return defaultLineHeight; } 17338 override int paddingRight() { return defaultLineHeight; } 17339 override int paddingLeft() { return defaultLineHeight; } 17340 17341 this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 17342 assert(onOK !is null); 17343 17344 t = initialData; 17345 17346 static if(is(T == class)) { 17347 if(t is null) 17348 t = new T(); 17349 } 17350 this.onOK = onOK; 17351 this.onCancel = onCancel; 17352 super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 17353 17354 static if(is(T == class)) 17355 this.addDataControllerWidget(t); 17356 else 17357 this.addDataControllerWidget(&t); 17358 17359 auto hl = new HorizontalLayout(this); 17360 auto stretch = new HorizontalSpacer(hl); // to right align 17361 auto ok = new CommandButton("OK", hl); 17362 auto cancel = new CommandButton("Cancel", hl); 17363 ok.addEventListener(EventType.triggered, &OK); 17364 cancel.addEventListener(EventType.triggered, &Cancel); 17365 17366 this.addEventListener((KeyDownEvent ev) { 17367 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 17368 ok.focus(); 17369 OK(); 17370 ev.preventDefault(); 17371 } 17372 if(ev.key == Key.Escape) { 17373 Cancel(); 17374 ev.preventDefault(); 17375 } 17376 }); 17377 17378 this.addEventListener((scope ClosedEvent ce) { 17379 if(onCancel) 17380 onCancel(); 17381 }); 17382 17383 //this.children[0].focus(); 17384 } 17385 17386 override void OK() { 17387 onOK(t); 17388 close(); 17389 } 17390 17391 override void Cancel() { 17392 if(onCancel) 17393 onCancel(); 17394 close(); 17395 } 17396 } 17397 17398 private template baseClassCount(Class) { 17399 private int helper() { 17400 int count = 0; 17401 static if(is(Class bases == super)) { 17402 foreach(base; bases) 17403 static if(is(base == class)) 17404 count += 1 + baseClassCount!base; 17405 } 17406 return count; 17407 } 17408 17409 enum int baseClassCount = helper(); 17410 } 17411 17412 private long stringToLong(string s) { 17413 long ret; 17414 if(s.length == 0) 17415 return ret; 17416 bool negative = s[0] == '-'; 17417 if(negative) 17418 s = s[1 .. $]; 17419 foreach(ch; s) { 17420 if(ch >= '0' && ch <= '9') { 17421 ret *= 10; 17422 ret += ch - '0'; 17423 } 17424 } 17425 if(negative) 17426 ret = -ret; 17427 return ret; 17428 } 17429 17430 17431 interface ReflectableProperties { 17432 /++ 17433 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 17434 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 17435 json in the current implementation. 17436 17437 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 17438 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 17439 as of the June 2, 2021 release. 17440 17441 History: 17442 Added June 2, 2021. 17443 17444 See_Also: [getPropertyAsString], [setPropertyFromString] 17445 +/ 17446 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 17447 /++ 17448 Requests a property to be delivered to you as a string, through your `sink` delegate. 17449 17450 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 17451 be interpreted as json, otherwise, it is just a plain string. 17452 17453 The sink should always be called exactly once for each call (it is basically a return value, but it might 17454 use a local buffer it maintains instead of allocating a return value). 17455 17456 History: 17457 Added June 2, 2021. 17458 17459 See_Also: [getPropertiesList], [setPropertyFromString] 17460 +/ 17461 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 17462 /++ 17463 Sets the given property, if it exists, to the given value, if possible. If `strIsJson` is true, it will json decode (if the implementation wants to) then apply the value, otherwise it will treat it as a plain string. 17464 17465 History: 17466 Added June 2, 2021. 17467 17468 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 17469 +/ 17470 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 17471 17472 /// [setPropertyFromString] possible return values 17473 enum SetPropertyResult { 17474 success = 0, /// the property has been successfully set to the request value 17475 notPermitted = -1, /// the property exists but it cannot be changed at this time 17476 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 17477 noSuchProperty = -3, /// there is no property by that name 17478 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 17479 invalidValue = -5, /// the string is in the correct format, but the specific given value could not be used (for example, because it was out of bounds) 17480 } 17481 17482 /++ 17483 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 17484 17485 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 17486 17487 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 17488 rarely need to use these building blocks directly. 17489 +/ 17490 mixin template RegisterSetters() { 17491 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 17492 switch(name) { 17493 foreach(memberName; __traits(derivedMembers, typeof(this))) { 17494 case memberName: 17495 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 17496 if(value != "true" && value != "false") 17497 return SetPropertyResult.wrongFormat; 17498 __traits(getMember, this, memberName) = value == "true" ? true : false; 17499 return SetPropertyResult.success; 17500 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 17501 import core.stdc.stdlib; 17502 char[128] zero = 0; 17503 if(buffer.length + 1 >= zero.length) 17504 return SetPropertyResult.wrongFormat; 17505 zero[0 .. buffer.length] = buffer[]; 17506 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 17507 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 17508 import core.stdc.stdlib; 17509 char[128] zero = 0; 17510 if(buffer.length + 1 >= zero.length) 17511 return SetPropertyResult.wrongFormat; 17512 zero[0 .. buffer.length] = buffer[]; 17513 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 17514 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 17515 __traits(getMember, this, memberName) = value.idup; 17516 } else { 17517 return SetPropertyResult.notImplemented; 17518 } 17519 17520 } 17521 default: 17522 return super.setPropertyFromString(name, value, valueIsJson); 17523 } 17524 } 17525 } 17526 17527 /++ 17528 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 17529 17530 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 17531 17532 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 17533 rarely need to use these building blocks directly. 17534 +/ 17535 mixin template RegisterGetters() { 17536 override void getPropertiesList(scope void delegate(string name) sink) const { 17537 super.getPropertiesList(sink); 17538 17539 foreach(memberName; __traits(derivedMembers, typeof(this))) { 17540 sink(memberName); 17541 } 17542 } 17543 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 17544 switch(name) { 17545 foreach(memberName; __traits(derivedMembers, typeof(this))) { 17546 case memberName: 17547 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 17548 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 17549 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 17550 import core.stdc.stdio; 17551 char[32] buffer; 17552 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 17553 sink(name, buffer[0 .. len], true); 17554 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 17555 import core.stdc.stdio; 17556 char[32] buffer; 17557 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 17558 sink(name, buffer[0 .. len], true); 17559 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 17560 sink(name, __traits(getMember, this, memberName), false); 17561 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 17562 } else { 17563 sink(name, null, true); 17564 } 17565 17566 return; 17567 } 17568 default: 17569 return super.getPropertyAsString(name, sink); 17570 } 17571 } 17572 } 17573 } 17574 17575 private struct Stack(T) { 17576 this(int maxSize) { 17577 internalLength = 0; 17578 arr = initialBuffer[]; 17579 } 17580 17581 ///. 17582 void push(T t) { 17583 if(internalLength >= arr.length) { 17584 auto oldarr = arr; 17585 if(arr.length < 4096) 17586 arr = new T[arr.length * 2]; 17587 else 17588 arr = new T[arr.length + 4096]; 17589 arr[0 .. oldarr.length] = oldarr[]; 17590 } 17591 17592 arr[internalLength] = t; 17593 internalLength++; 17594 } 17595 17596 ///. 17597 T pop() { 17598 assert(internalLength); 17599 internalLength--; 17600 return arr[internalLength]; 17601 } 17602 17603 ///. 17604 T peek() { 17605 assert(internalLength); 17606 return arr[internalLength - 1]; 17607 } 17608 17609 ///. 17610 @property bool empty() { 17611 return internalLength ? false : true; 17612 } 17613 17614 ///. 17615 private T[] arr; 17616 private size_t internalLength; 17617 private T[64] initialBuffer; 17618 // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), 17619 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 17620 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 17621 } 17622 17623 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. 17624 private struct WidgetStream { 17625 17626 ///. 17627 @property Widget front() { 17628 return current.widget; 17629 } 17630 17631 /// Use Widget.tree instead. 17632 this(Widget start) { 17633 current.widget = start; 17634 current.childPosition = -1; 17635 isEmpty = false; 17636 stack = typeof(stack)(0); 17637 } 17638 17639 /* 17640 Handle it 17641 handle its children 17642 17643 */ 17644 17645 ///. 17646 void popFront() { 17647 more: 17648 if(isEmpty) return; 17649 17650 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 17651 17652 current.childPosition++; 17653 if(current.childPosition >= current.widget.children.length) { 17654 if(stack.empty()) 17655 isEmpty = true; 17656 else { 17657 current = stack.pop(); 17658 goto more; 17659 } 17660 } else { 17661 stack.push(current); 17662 current.widget = current.widget.children[current.childPosition]; 17663 current.childPosition = -1; 17664 } 17665 } 17666 17667 ///. 17668 @property bool empty() { 17669 return isEmpty; 17670 } 17671 17672 private: 17673 17674 struct Current { 17675 Widget widget; 17676 int childPosition; 17677 } 17678 17679 Current current; 17680 17681 Stack!(Current) stack; 17682 17683 bool isEmpty; 17684 } 17685 17686 17687 /+ 17688 17689 I could fix up the hierarchy kinda like this 17690 17691 class Widget { 17692 Widget[] children() { return null; } 17693 } 17694 interface WidgetContainer { 17695 Widget asWidget(); 17696 void addChild(Widget w); 17697 17698 // alias asWidget this; // but meh 17699 } 17700 17701 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 17702 17703 class Layout : Widget, WidgetContainer {} 17704 17705 class Window : WidgetContainer {} 17706 17707 17708 All constructors that previously took Widgets should now take WidgetContainers instead 17709 17710 17711 17712 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". 17713 +/ 17714 17715 /+ 17716 LAYOUTS 2.0 17717 17718 can just be assigned as a function. assigning a new one will cause it to be immediately called. 17719 17720 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 17721 17722 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 17723 17724 and even Paint can just use computedStyle... 17725 17726 background color 17727 font 17728 border color and style 17729 17730 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 17731 please note that many widgets and in some modes will completely ignore properties as they will. 17732 they are just hints you set, not promises. 17733 17734 17735 17736 17737 17738 So generally the existing virtual functions are just the default for the class. But individual objects 17739 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 17740 +/ 17741 17742 /++ 17743 Structure to represent a collection of background hints. New features can be added here, so make sure you use the provided constructors and factories for maximum compatibility. 17744 17745 History: 17746 Added May 24, 2021. 17747 +/ 17748 struct WidgetBackground { 17749 /++ 17750 A background with the given solid color. 17751 +/ 17752 this(Color color) { 17753 this.color = color; 17754 } 17755 17756 this(WidgetBackground bg) { 17757 this = bg; 17758 } 17759 17760 /++ 17761 Creates a widget from the string. 17762 17763 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 17764 +/ 17765 static WidgetBackground fromString(string s) { 17766 return WidgetBackground(Color.fromString(s)); 17767 } 17768 17769 /++ 17770 The background is not necessarily a solid color, but you can always specify a color as a fallback. 17771 17772 History: 17773 Made `public` on December 18, 2022 (dub v10.10). 17774 +/ 17775 Color color; 17776 } 17777 17778 /++ 17779 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!) 17780 17781 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. 17782 17783 You should not inherit from this directly, but instead use [VisualTheme]. 17784 17785 History: 17786 Added May 8, 2021 17787 +/ 17788 abstract class BaseVisualTheme { 17789 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 17790 abstract void doPaint(Widget widget, WidgetPainter painter); 17791 17792 /+ 17793 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 17794 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 17795 +/ 17796 17797 /++ 17798 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 17799 where the interpretation of the string varies for each property and may include things like measurement units. 17800 +/ 17801 abstract string getPropertyString(Widget widget, string propertyName); 17802 17803 /++ 17804 Default background color of the window. Widgets also use this to simulate transparency. 17805 17806 Probably some shade of grey. 17807 +/ 17808 abstract Color windowBackgroundColor(); 17809 abstract Color widgetBackgroundColor(); 17810 abstract Color foregroundColor(); 17811 abstract Color lightAccentColor(); 17812 abstract Color darkAccentColor(); 17813 17814 /++ 17815 Colors used to indicate active selections in lists and text boxes, etc. 17816 +/ 17817 abstract Color selectionForegroundColor(); 17818 /// ditto 17819 abstract Color selectionBackgroundColor(); 17820 17821 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 17822 17823 /++ 17824 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 17825 +/ 17826 abstract OperatingSystemFont defaultFont(int dpi); 17827 17828 private OperatingSystemFont[int] defaultFontCache_; 17829 private OperatingSystemFont defaultFontCached(int dpi) { 17830 if(dpi !in defaultFontCache_) { 17831 // FIXME: set this to false if X disconnect or if visual theme changes 17832 defaultFontCache_[dpi] = defaultFont(dpi); 17833 } 17834 return defaultFontCache_[dpi]; 17835 } 17836 } 17837 17838 /+ 17839 A widget should have: 17840 classList 17841 dataset 17842 attributes 17843 computedStyles 17844 state (persistent) 17845 dynamic state (focused, hover, etc) 17846 +/ 17847 17848 // visualTheme.computedStyle(this).paddingLeft 17849 17850 17851 /++ 17852 This is your entry point to create your own visual theme for custom widgets. 17853 17854 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 17855 17856 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 17857 +/ 17858 abstract class VisualTheme(CRTP) : BaseVisualTheme { 17859 override string getPropertyString(Widget widget, string propertyName) { 17860 return null; 17861 } 17862 17863 /+ 17864 mixin StyleOverride!Widget 17865 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 17866 w.useStyleProperties(dg); 17867 } 17868 +/ 17869 17870 final override void doPaint(Widget widget, WidgetPainter painter) { 17871 auto derived = cast(CRTP) cast(void*) this; 17872 17873 scope void delegate(Widget, WidgetPainter) bestMatch; 17874 int bestMatchScore; 17875 17876 static if(__traits(hasMember, CRTP, "paint")) 17877 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 17878 static if(is(typeof(overload) Params == __parameters)) { 17879 static assert(Params.length == 2); 17880 static assert(is(Params[0] : Widget)); 17881 static assert(is(Params[1] == WidgetPainter)); 17882 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); 17883 17884 alias type = Params[0]; 17885 if(cast(type) widget) { 17886 auto score = baseClassCount!type; 17887 17888 if(score > bestMatchScore) { 17889 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 17890 bestMatchScore = score; 17891 } 17892 } 17893 } else static assert(0, "paint should be a method."); 17894 } 17895 17896 if(bestMatch) 17897 bestMatch(widget, painter); 17898 else 17899 widget.paint(painter); 17900 } 17901 17902 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 17903 17904 // 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 17905 // mixin Beautiful95Theme; 17906 mixin DefaultLightTheme; 17907 17908 private static struct Cached { 17909 // i prolly want to do this 17910 } 17911 } 17912 17913 /// ditto 17914 mixin template Beautiful95Theme() { 17915 override Color windowBackgroundColor() { return Color(212, 212, 212); } 17916 override Color widgetBackgroundColor() { return Color.white; } 17917 override Color foregroundColor() { return Color.black; } 17918 override Color darkAccentColor() { return Color(172, 172, 172); } 17919 override Color lightAccentColor() { return Color(223, 223, 223); } 17920 override Color selectionForegroundColor() { return Color.white; } 17921 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 17922 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 17923 } 17924 17925 /// ditto 17926 mixin template DefaultLightTheme() { 17927 override Color windowBackgroundColor() { return Color(232, 232, 232); } 17928 override Color widgetBackgroundColor() { return Color.white; } 17929 override Color foregroundColor() { return Color.black; } 17930 override Color darkAccentColor() { return Color(172, 172, 172); } 17931 override Color lightAccentColor() { return Color(223, 223, 223); } 17932 override Color selectionForegroundColor() { return Color.white; } 17933 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 17934 override OperatingSystemFont defaultFont(int dpi) { 17935 version(Windows) 17936 return new OperatingSystemFont("Segoe UI"); 17937 else static if(UsingSimpledisplayCocoa) { 17938 return (new OperatingSystemFont()).loadDefault; 17939 } else { 17940 // FIXME: undo xft's scaling so we don't end up double scaled 17941 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 17942 } 17943 } 17944 } 17945 17946 /// ditto 17947 mixin template DefaultDarkTheme() { 17948 override Color windowBackgroundColor() { return Color(64, 64, 64); } 17949 override Color widgetBackgroundColor() { return Color.black; } 17950 override Color foregroundColor() { return Color.white; } 17951 override Color darkAccentColor() { return Color(20, 20, 20); } 17952 override Color lightAccentColor() { return Color(80, 80, 80); } 17953 override Color selectionForegroundColor() { return Color.white; } 17954 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 17955 override OperatingSystemFont defaultFont(int dpi) { 17956 version(Windows) 17957 return new OperatingSystemFont("Segoe UI", 12); 17958 else static if(UsingSimpledisplayCocoa) { 17959 return (new OperatingSystemFont()).loadDefault; 17960 } else { 17961 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 17962 } 17963 } 17964 } 17965 17966 /// ditto 17967 alias DefaultTheme = DefaultLightTheme; 17968 17969 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 17970 /+ 17971 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 17972 Color windowBackgroundColor() { return Color(242, 242, 242); } 17973 Color darkAccentColor() { return windowBackgroundColor; } 17974 Color lightAccentColor() { return windowBackgroundColor; } 17975 +/ 17976 } 17977 17978 /++ 17979 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 17980 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 17981 17982 History: 17983 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 17984 +/ 17985 class StateChanged(alias field) : Event { 17986 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 17987 override bool cancelable() const { return false; } 17988 this(Widget target, typeof(field) newValue) { 17989 this.newValue = newValue; 17990 super(EventString, target); 17991 } 17992 17993 typeof(field) newValue; 17994 } 17995 17996 /++ 17997 Convenience function to add a `triggered` event listener. 17998 17999 Its implementation is simply `w.addEventListener("triggered", dg);` 18000 18001 History: 18002 Added November 27, 2021 (dub v10.4) 18003 +/ 18004 void addWhenTriggered(Widget w, void delegate() dg) { 18005 w.addEventListener("triggered", dg); 18006 } 18007 18008 /++ 18009 Observable varables can be added to widgets and when they are changed, it fires 18010 off a [StateChanged] event so you can react to it. 18011 18012 It is implemented as a getter and setter property, along with another helper you 18013 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 18014 event through the usual means. Just give the name of the variable. See [StateChanged] for an 18015 example. 18016 18017 History: 18018 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 18019 +/ 18020 mixin template Observable(T, string name) { 18021 private T backing; 18022 18023 mixin(q{ 18024 void } ~ name ~ q{_changed (void delegate(T) dg) { 18025 this.addEventListener((StateChanged!this_thing ev) { 18026 dg(ev.newValue); 18027 }); 18028 } 18029 18030 @property T } ~ name ~ q{ () { 18031 return backing; 18032 } 18033 18034 @property void } ~ name ~ q{ (T t) { 18035 backing = t; 18036 auto event = new StateChanged!this_thing(this, t); 18037 event.dispatch(); 18038 } 18039 }); 18040 18041 mixin("private alias this_thing = " ~ name ~ ";"); 18042 } 18043 18044 18045 private bool startsWith(string test, string thing) { 18046 if(test.length < thing.length) 18047 return false; 18048 return test[0 .. thing.length] == thing; 18049 } 18050 18051 private bool endsWith(string test, string thing) { 18052 if(test.length < thing.length) 18053 return false; 18054 return test[$ - thing.length .. $] == thing; 18055 } 18056 18057 /++ 18058 Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon` 18059 18060 Note they can NOT have accelerators or toolbars; those annotations will be ignored. 18061 18062 Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time. 18063 18064 See_Also: 18065 [Widget.setMenuAndToolbarFromAnnotatedCode] 18066 +/ 18067 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) { 18068 return createContextMenuFromAnnotatedCode(w, w); 18069 } 18070 18071 /// ditto 18072 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) { 18073 return createContextMenuFromAnnotatedCode_internal(w, t); 18074 } 18075 /// ditto 18076 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) { 18077 return createContextMenuFromAnnotatedCode_internal(w, t); 18078 } 18079 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) { 18080 Menu ret = new Menu("", w); 18081 18082 foreach(memberName; __traits(derivedMembers, T)) { 18083 static if(memberName != "this") 18084 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 18085 .menu menu; 18086 bool separator; 18087 .hotkey hotkey; 18088 .icon icon; 18089 string label; 18090 string tip; 18091 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 18092 static if(is(typeof(attr) == .menu)) 18093 menu = attr; 18094 else static if(is(attr == .separator)) 18095 separator = true; 18096 else static if(is(typeof(attr) == .hotkey)) 18097 hotkey = attr; 18098 else static if(is(typeof(attr) == .icon)) 18099 icon = attr; 18100 else static if(is(typeof(attr) == .label)) 18101 label = attr.label; 18102 else static if(is(typeof(attr) == .tip)) 18103 tip = attr.tip; 18104 } 18105 18106 if(menu is .menu.init) { 18107 ushort correctIcon = icon.id; // FIXME 18108 if(label.length == 0) 18109 label = memberName.toMenuLabel; 18110 18111 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName)); 18112 18113 auto action = new Action(label, correctIcon, handler); 18114 18115 if(separator) 18116 ret.addSeparator(); 18117 ret.addItem(new MenuItem(action)); 18118 } 18119 } 18120 } 18121 18122 return ret; 18123 } 18124 18125 // still do layout delegation 18126 // and... split off Window from Widget. 18127 18128 version(minigui_screenshots) 18129 struct Screenshot { 18130 string name; 18131 } 18132 18133 version(minigui_screenshots) 18134 static if(__VERSION__ > 2092) 18135 mixin(q{ 18136 shared static this() { 18137 import core.runtime; 18138 18139 static UnitTestResult screenshotMagic() { 18140 string name; 18141 18142 import arsd.png; 18143 18144 auto results = new Window(); 18145 auto button = new Button("do it", results); 18146 18147 Window.newWindowCreated = delegate(Window w) { 18148 Timer timer; 18149 timer = new Timer(250, { 18150 auto img = w.win.takeScreenshot(); 18151 timer.destroy(); 18152 18153 version(Windows) 18154 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 18155 else 18156 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 18157 18158 w.close(); 18159 }); 18160 }; 18161 18162 button.addWhenTriggered( { 18163 18164 foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) { 18165 name = null; 18166 static foreach(attr; __traits(getAttributes, test)) { 18167 static if(is(typeof(attr) == Screenshot)) 18168 name = attr.name; 18169 } 18170 if(name.length) { 18171 test(); 18172 } 18173 } 18174 18175 }); 18176 18177 results.loop(); 18178 18179 return UnitTestResult(0, 0, false, false); 18180 } 18181 18182 18183 Runtime.extendedModuleUnitTester = &screenshotMagic; 18184 } 18185 }); 18186 version(minigui_screenshots) { 18187 version(unittest) 18188 void main() {} 18189 else static assert(0, "dont forget the -unittest flag to dmd"); 18190 } 18191 18192 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 18193 // FIXME: make multiple accelerators disambiguate based ona rgs 18194 // FIXME: MainWindow ctor should have same arg order as Window 18195 // FIXME: mainwindow ctor w/ client area size instead of total size. 18196 // Push on/off button (basically an alternate display of a checkbox) -- BS_PUSHLIKE and maybe BS_TEXT (BS_TOP moves it). see also BS_FLAT. 18197 // FIXME: tri-state checkbox 18198 // FIXME: subordinate controls grouping...