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 // FIXME: checkbox menus and submenus and stuff 25 26 // FOXME: look at Windows rebar control too 27 28 /* 29 30 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 31 32 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 33 */ 34 35 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 36 37 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 38 39 // FIXME: opt-in file picker widget with image support 40 41 // FIXME: number widget 42 43 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 44 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 45 46 // osx style menu search. 47 48 // would be cool for a scroll bar to have marking capabilities 49 // kinda like vim's marks just on clicks etc and visual representation 50 // generically. may be cool to add an up arrow to the bottom too 51 // 52 // leave a shadow of where you last were for going back easily 53 54 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 55 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 56 // the window. 57 58 // so what about context menus? 59 60 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 61 62 // FIXME: make the scroll thing go to bottom when the content changes. 63 64 // 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 65 66 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 67 68 69 // FIXME: add a command search thingy built in and implement tip. 70 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 71 72 // On Windows: 73 // FIXME: various labels look broken in high contrast mode 74 // FIXME: changing themes while the program is upen doesn't trigger a redraw 75 76 // add note about manifest to documentation. also icons. 77 78 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 79 // FIXME: clear the corner of scrollbars if they pop up 80 81 // minigui needs to have a stdout redirection for gui mode on windows writeln 82 83 // I kinda wanna do state reacting. sort of. idk tho 84 85 // need a viewer widget that works like a web page - arrows scroll down consistently 86 87 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 88 89 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 90 // and help info about menu items. 91 // and search in menus? 92 93 // FIXME: a scroll area event signaling when a thing comes into view might be good 94 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 95 96 // FIXME: unify Windows style line endings 97 98 /* 99 TODO: 100 101 pie menu 102 103 class Form with submit behavior -- see AutomaticDialog 104 105 disabled widgets and menu items 106 107 event cleanup 108 tooltips. 109 api improvements 110 111 margins are kinda broken, they don't collapse like they should. at least. 112 113 a table form btw would be a horizontal layout of vertical layouts holding each column 114 that would give the same width things 115 */ 116 117 /* 118 119 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 120 */ 121 122 /++ 123 minigui is a smallish GUI widget library, aiming to be on par with at least 124 HTML4 forms and a few other expected gui components. It uses native controls 125 on Windows and does its own thing on Linux (Mac is not currently supported but 126 I'm slowly working on it). 127 128 129 $(H3 Conceptual Overviews) 130 131 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. 132 133 $(H4 Code structure) 134 135 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. 136 137 --- 138 import arsd.minigui; 139 140 void main() { 141 // first, create a window, the (optional) string here is its title 142 auto window = new MainWindow("Hello, World!"); 143 144 // lay out some widgets inside the window to create the ui 145 auto name = new LabeledLineEdit("What is your name?", window); 146 auto button = new Button("Say Hello", window); 147 148 // prepare event handlers 149 button.addEventListener(EventType.triggered, () { 150 window.messageBox("Hello, " ~ name.content ~ "!"); 151 }); 152 153 // show the window and run the event loop until this window is closed 154 window.loop(); 155 } 156 --- 157 158 To compile, run `opend hello.d`, then run the generated `hello` program. 159 160 While the specifics will change, nearly all minigui applications will roughly follow this pattern. 161 162 $(TIP 163 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. 164 165 You may call this if you don't have a single main window. 166 167 Even a basic minigui window can benefit from these if you don't have a single main window: 168 169 --- 170 import arsd.minigui; 171 172 void main() { 173 // create a struct to hold gathered info 174 struct Hello { string name; } 175 // let minigui create a dialog box to get that 176 // info from the user. If you have a main window, 177 // you'd pass that here, but it is not required 178 dialog((Hello info) { 179 // inline handler of the "OK" button 180 messageBox("Hello, " ~ info.name); 181 }); 182 183 // since there is no main window to loop on, 184 // we instead call the event loop singleton ourselves 185 EventLoop.get.run; 186 } 187 --- 188 189 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! 190 ) 191 192 $(H4 How to lay out widgets) 193 194 To better understand the details of layout algorithms and see more available included classes, see [Layout]. 195 196 $(H5 Default layouts) 197 198 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. 199 200 $(TIP 201 minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off. 202 ) 203 204 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. 205 206 $(NOTE 207 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. 208 ) 209 210 For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]: 211 212 --- 213 import arsd.minigui; 214 void main() { 215 auto window = new MainWindow(); 216 217 // make the layout a child of our window 218 auto hl = new HorizontalLayout(window); 219 220 // then make the widgets children of the layout 221 auto leftButton = new Button("Left", hl); 222 auto rightButton = new Button("Right", hl); 223 224 window.loop(); 225 } 226 --- 227 228 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. 229 230 $(H5 Nesting layouts) 231 232 Nesting layouts lets you carve up the rectangle in different ways. 233 234 $(EMBED_UNITTEST layout-example) 235 236 $(H5 Special layouts) 237 238 [TabWidget] can show pages of layouts as tabs. 239 240 See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead. 241 242 $(H5 Other common layout classes) 243 244 [HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout] 245 246 $(H4 How to respond to widget events) 247 248 To better understanding the underlying event system, see [Event]. 249 250 Each widget emits its own events, which propagate up through their parents until they reach their top-level window. 251 252 $(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.) 253 254 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! 255 256 See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example. 257 258 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)). 259 260 $(TIP 261 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. 262 ) 263 264 All windows also have titles. You can change this at any time with the `window.title = "string";` property. 265 266 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.) 267 268 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. 269 270 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. 271 272 Other parts can be added by you and are under your control. You add them with: 273 274 --- 275 window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units); 276 --- 277 278 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. 279 280 You may prefer to set them all at once, with: 281 282 --- 283 window.statusBar.parts.setSizes(1, 1, 1); 284 --- 285 286 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. 287 288 You should call this right after creating your `MainWindow` as part of your setup code. 289 290 Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";` 291 292 $(NOTE 293 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. 294 ) 295 296 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! 297 298 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. 299 300 $(H4 How to do custom styles) 301 302 Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s. 303 304 $(WARNING 305 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. 306 307 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. 308 309 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. 310 ) 311 312 See [Widget.Style] for more information. 313 314 $(H4 Selection of categorized widgets) 315 316 $(LIST 317 * Buttons: [Button] 318 * Text display widgets: [TextLabel], [TextDisplay] 319 * Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit] 320 * Selecting multiple on/off options: [Checkbox] 321 * Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection] 322 * Getting rough numeric input: [HorizontalSlider], [VerticalSlider] 323 * Displaying data: [ImageBox], [ProgressBar], [TableView] 324 * Showing a list of editable items: [GenericListViewWidget] 325 * Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget] 326 ) 327 328 And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons]. 329 330 If none of these do what you need, you'll want to write your own. More on that in the following section. 331 332 $(H4 custom widgets - how to write your own) 333 334 See some example programs: https://github.com/adamdruppe/minigui-samples 335 336 When you can't build your application out of existing widgets, you'll want to make your own. The general pattern is to subclass [Widget], write a constructor that takes a `Widget` parent argument you pass to `super`, then set some values, override methods you want to customize, and maybe add child widgets and events as appropriate. You might also be able to subclass an existing other Widget and customize that way. 337 338 To get more specific, let's consider a few illustrative examples, then we'll come back to some principles. 339 340 $(H5 Custom Widget Examples) 341 342 $(H5 More notes) 343 344 See [Widget]. 345 346 If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too! 347 348 If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required. 349 350 See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points. 351 352 You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada. 353 354 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!) 355 356 $(TIP 357 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. 358 ) 359 360 $(H5 Timers and animations) 361 362 The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler. 363 364 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. 365 366 $(H5 Clipboard integrations, drag and drop) 367 368 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. 369 370 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. 371 372 See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay. 373 374 $(H5 Context menus) 375 376 Override [Widget.contextMenu] in your subclass. 377 378 $(H4 Coming later) 379 380 Among the unfinished features: unified selections, translateable strings, external integrations. 381 382 $(H2 Running minigui programs) 383 384 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. 385 386 $(H2 Building minigui programs) 387 388 minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and 389 [arsd.textlayouter], on which it is built. simpledisplay provides the low-level 390 interfaces and minigui builds the concept of widgets inside the windows on top of it. 391 392 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 393 It isn't hugely concerned with appearance - on Windows, it just uses the native 394 controls and native theme, and on Linux, it keeps it simple and I may change that 395 at any time, though after May 2021, you can customize some things with css-inspired 396 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 397 you can use the custom implementation there too, but... you shouldn't.) 398 399 The event model is similar to what you use in the browser with Javascript and the 400 layout engine tries to automatically fit things in, similar to a css flexbox. 401 402 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 403 `-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead 404 of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w". 405 406 Otherwise you'll get a console and possibly other visual bugs. But if you do use 407 the subsystem:windows, note that Phobos' writeln will crash the program! 408 409 HTML_To_Classes: 410 $(SMALL_TABLE 411 HTML Code | Minigui Class 412 413 `<input type="text">` | [LineEdit] 414 `<input type="password">` | [PasswordEdit] 415 `<textarea>` | [TextEdit] 416 `<select>` | [DropDownSelection] 417 `<input type="checkbox">` | [Checkbox] 418 `<input type="radio">` | [Radiobox] 419 `<button>` | [Button] 420 ) 421 422 423 Stretchiness: 424 The default is 4. You can use larger numbers for things that should 425 consume a lot of space, and lower numbers for ones that are better at 426 smaller sizes. 427 428 Overlapped_input: 429 COMING EVENTUALLY: 430 minigui will include a little bit of I/O functionality that just works 431 with the event loop. If you want to get fancy, I suggest spinning up 432 another thread and posting events back and forth. 433 434 $(H2 Add ons) 435 See the `minigui_addons` directory in the arsd repo for some add on widgets 436 you can import separately too. 437 438 $(H3 XML definitions) 439 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 440 441 $(H3 Scriptability) 442 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 443 in this documentation, it means you can call it from the script language. 444 445 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 446 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 447 448 --- 449 import arsd.minigui_xml; 450 import arsd.script; 451 452 var globals = var.emptyObject; 453 globals.makeWidgetFromString = &makeWidgetFromString; 454 455 // this now works 456 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 457 --- 458 459 More to come. 460 461 Widget_tree_notes: 462 minigui doesn't really formalize these distinctions, but in practice, there are multiple types of widgets: 463 464 $(LIST 465 * Containers - a widget that holds other widgets directly, generally [Layout]s. [WidgetContainer] is an attempt to formalize this but is nothing really special. 466 467 * Reparenting containers - a widget that holds other widgets inside a different one of their parents. [MainWindow] is an example - any time you try to add a child to the main window, it actually goes to a special container one layer deeper. [ScrollMessageWidget] also works this way. 468 469 --- 470 auto child = new Widget(mainWindow); 471 assert(child.parent is mainWindow); // fails, its actual parent is mainWindow's inner container instead. 472 --- 473 474 * Limiting containers - a widget that can only hold children of a particular type. See [TabWidget], which can only hold [TabWidgetPage]s. 475 476 * Simple controls - a widget that cannot have children, but instead does a specific job. 477 478 * Compound controls - a widget that is comprised of children internally to help it do a specific job, but externally acts like a simple control that does not allow any more children. Ideally, this is encapsulated, but in practice, it leaks right now. 479 ) 480 481 In practice, all of these are [Widget]s right now, but this violates the OOP principles of substitutability since some operations are not actually valid on all subclasses. 482 483 Future breaking changes might be related to making this more structured but im not sure it is that important to actually break stuff over. 484 485 My_UI_Guidelines: 486 Note that the Linux custom widgets generally aim to be efficient on remote X network connections. 487 488 In a perfect world, you'd achieve all the following goals: 489 490 $(LIST 491 * All operations are present in the menu 492 * The operations the user wants at the moment are right where they want them 493 * All operations can be scripted 494 * The UI does not move any elements without explicit user action 495 * All numbers can be seen and typed in if wanted, even if the ui usually hides them 496 ) 497 498 $(H2 Future Directions) 499 500 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. 501 502 History: 503 In January 2025 (dub v12.0), minigui got a few more breaking changes: 504 505 $(LIST 506 * `defaultEventHandler_*` functions take more specific objects. So if you see errors like: 507 508 --- 509 Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`? 510 --- 511 512 Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary to be compatible with this change. 513 514 * Most event classes, except those explicitly used as a base class, are now marked `final`. If you depended on this subclassing, let me know and I'll see what I can do, but I expect there's little use of it. I now recommend all event classes the `final` unless you are specifically planning on extending it. 515 ) 516 517 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 518 519 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 520 tag this as version 2.0. 521 522 Among the changes: 523 $(LIST 524 * 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. 525 526 See [Event] for details. 527 528 * 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. 529 530 See [DoubleClickEvent] for details. 531 532 * 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. 533 534 See [Widget.Style] for details. 535 536 * 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. 537 538 * 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. 539 540 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 541 542 * 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. 543 544 * Various non-breaking additions. 545 ) 546 +/ 547 module arsd.minigui; 548 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 549 550 /++ 551 This hello world sample will have an oversized button, but that's ok, you see your first window! 552 +/ 553 version(Demo) 554 unittest { 555 import arsd.minigui; 556 557 void main() { 558 auto window = new MainWindow(); 559 560 // note the parent widget is almost always passed as the last argument to a constructor 561 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 562 auto button = new Button("Close", window); 563 button.addWhenTriggered({ 564 window.close(); 565 }); 566 567 window.loop(); 568 } 569 570 main(); // exclude from docs 571 } 572 573 /++ 574 $(ID layout-example) 575 576 This example shows one way you can partition your window into a header 577 and sidebar. Here, the header and sidebar have a fixed width, while the 578 rest of the content sizes with the window. 579 580 It might be a new way of thinking about window layout to do things this 581 way - perhaps [GridLayout] more matches your style of thought - but the 582 concept here is to partition the window into sub-boxes with a particular 583 size, then partition those boxes into further boxes. 584 585 $(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.) 586 587 So to make the header, start with a child layout that has a max height. 588 It will use that space from the top, then the remaining children will 589 split the remaining area, meaning you can think of is as just being another 590 box you can split again. Keep splitting until you have the look you desire. 591 +/ 592 // https://github.com/adamdruppe/arsd/issues/310 593 version(minigui_screenshots) 594 @Screenshot("layout") 595 unittest { 596 import arsd.minigui; 597 598 // This helper class is just to help make the layout boxes visible. 599 // think of it like a <div style="background-color: whatever;"></div> in HTML. 600 class ColorWidget : Widget { 601 this(Color color, Widget parent) { 602 this.color = color; 603 super(parent); 604 } 605 Color color; 606 class Style : Widget.Style { 607 override WidgetBackground background() { return WidgetBackground(color); } 608 } 609 mixin OverrideStyle!Style; 610 } 611 612 void main() { 613 auto window = new Window; 614 615 // the key is to give it a max height. This is one way to do it: 616 auto header = new class HorizontalLayout { 617 this() { super(window); } 618 override int maxHeight() { return 50; } 619 }; 620 // this next line is a shortcut way of doing it too, but it only works 621 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 622 // is good to know how to make a new class like above anyway. 623 // auto header = new HorizontalLayout(50, window); 624 625 auto bar = new HorizontalLayout(window); 626 627 // or since this is so common, VerticalLayout and HorizontalLayout both 628 // can just take an argument in their constructor for max width/height respectively 629 630 // (could have tone this above too, but I wanted to demo both techniques) 631 auto left = new VerticalLayout(100, bar); 632 633 // and this is the main section's container. A plain Widget instance is good enough here. 634 auto container = new Widget(bar); 635 636 // and these just add color to the containers we made above for the screenshot. 637 // in a real application, you can just add your actual controls instead of these. 638 auto headerColorBox = new ColorWidget(Color.teal, header); 639 auto leftColorBox = new ColorWidget(Color.green, left); 640 auto rightColorBox = new ColorWidget(Color.purple, container); 641 642 window.loop(); 643 } 644 645 main(); // exclude from docs 646 } 647 648 649 import arsd.core; 650 import arsd.textlayouter; 651 652 alias Timer = arsd.simpledisplay.Timer; 653 public import arsd.simpledisplay; 654 /++ 655 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 656 657 History: 658 Was private until May 15, 2021. 659 +/ 660 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 661 662 version(Windows) { 663 import core.sys.windows.winnls; 664 import core.sys.windows.windef; 665 import core.sys.windows.basetyps; 666 import core.sys.windows.winbase; 667 import core.sys.windows.winuser; 668 import core.sys.windows.wingdi; 669 static import gdi = core.sys.windows.wingdi; 670 } 671 672 version(Windows) { 673 // to swap the default 674 // version(minigui_manifest) {} else version=minigui_no_manifest; 675 676 version(minigui_no_manifest) {} else { 677 version(D_OpenD) { 678 // OpenD always supports it 679 version=UseManifestMinigui; 680 } else { 681 version(CRuntime_Microsoft) // FIXME: mingw? 682 version=UseManifestMinigui; 683 } 684 685 } 686 687 688 version(UseManifestMinigui) { 689 // assume we want commctrl6 whenever possible since there's really no reason not to 690 // and this avoids some of the manifest hassle 691 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 692 } 693 } 694 695 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 696 private bool lastDefaultPrevented; 697 698 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 699 alias scriptable = arsd_jsvar_compatible; 700 701 version(Windows) { 702 // use native widgets when available unless specifically asked otherwise 703 version(custom_widgets) { 704 enum bool UsingCustomWidgets = true; 705 enum bool UsingWin32Widgets = false; 706 } else { 707 version = win32_widgets; 708 enum bool UsingCustomWidgets = false; 709 enum bool UsingWin32Widgets = true; 710 } 711 // and native theming when needed 712 //version = win32_theming; 713 } else { 714 enum bool UsingCustomWidgets = true; 715 enum bool UsingWin32Widgets = false; 716 version=custom_widgets; 717 } 718 719 720 721 /* 722 723 The main goals of minigui.d are to: 724 1) Provide basic widgets that just work in a lightweight lib. 725 I basically want things comparable to a plain HTML form, 726 plus the easy and obvious things you expect from Windows 727 apps like a menu. 728 2) Use native things when possible for best functionality with 729 least library weight. 730 3) Give building blocks to provide easy extension for your 731 custom widgets, or hooking into additional native widgets 732 I didn't wrap. 733 4) Provide interfaces for easy interaction between third 734 party minigui extensions. (event model, perhaps 735 signals/slots, drop-in ease of use bits.) 736 5) Zero non-system dependencies, including Phobos as much as 737 I reasonably can. It must only import arsd.color and 738 my simpledisplay.d. If you need more, it will have to be 739 an extension module. 740 6) An easy layout system that generally works. 741 742 A stretch goal is to make it easy to make gui forms with code, 743 some kind of resource file (xml?) and even a wysiwyg designer. 744 745 Another stretch goal is to make it easy to hook data into the gui, 746 including from reflection. So like auto-generate a form from a 747 function signature or struct definition, or show a list from an 748 array that automatically updates as the array is changed. Then, 749 your program focuses on the data more than the gui interaction. 750 751 752 753 STILL NEEDED: 754 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 755 * slider 756 * listbox 757 * spinner 758 * label? 759 * rich text 760 */ 761 762 763 /+ 764 enum LayoutMethods { 765 verticalFlex, 766 horizontalFlex, 767 inlineBlock, // left to right, no stretch, goes to next line as needed 768 static, // just set to x, y 769 verticalNoStretch, // browser style default 770 771 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 772 773 grid, // magic 774 } 775 +/ 776 777 /++ 778 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. 779 780 781 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. 782 783 --- 784 class MinimalWidget : Widget { 785 this(Widget parent) { 786 super(parent); 787 } 788 } 789 --- 790 791 $(SIDEBAR 792 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. 793 ) 794 795 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. 796 797 Among the things you'll most likely want to change in your custom widget: 798 799 $(LIST 800 * 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.) 801 802 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. 803 804 Do this $(I after) calling the `super` constructor. 805 806 * 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. 807 808 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 809 810 * 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. 811 812 * 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. 813 ) 814 815 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. 816 817 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 818 819 Your own custom-drawn and native system controls can exist side-by-side. 820 821 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. 822 +/ 823 class Widget : ReflectableProperties { 824 825 private int toolbarIconSize() { 826 return scaleWithDpi(24); 827 } 828 829 830 /++ 831 Returns the current size of the widget. 832 833 History: 834 Added January 3, 2025 835 +/ 836 final Size size() const { 837 return Size(width, height); 838 } 839 840 private bool willDraw() { 841 return true; 842 } 843 844 /+ 845 /++ 846 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 847 848 History: 849 Added September 15, 2021 850 implemented.... ??? 851 +/ 852 void prepareReflection(this This)() { 853 854 } 855 +/ 856 857 private bool _enabled = true; 858 859 /++ 860 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. 861 862 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. 863 864 History: 865 Added November 23, 2021 (dub v10.4) 866 867 Warning: the specific behavior of disabling with parents may change in the future. 868 Bugs: 869 Currently only implemented for widgets backed by native Windows controls. 870 871 See_Also: [disabledReason], [disabledBy] 872 +/ 873 @property bool enabled() { 874 return disabledBy() is null; 875 } 876 877 /// ditto 878 @property void enabled(bool yes) { 879 _enabled = yes; 880 version(win32_widgets) { 881 if(hwnd) 882 EnableWindow(hwnd, yes); 883 } 884 setDynamicState(DynamicState.disabled, yes); 885 redraw(); 886 } 887 888 private string disabledReason_; 889 890 /++ 891 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. 892 893 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 894 895 History: 896 Added November 23, 2021 (dub v10.4) 897 See_Also: [enabled], [disabledBy] 898 +/ 899 @property string disabledReason() { 900 auto w = disabledBy(); 901 return (w is null) ? null : w.disabledReason_; 902 } 903 904 /// ditto 905 @property void disabledReason(string reason) { 906 disabledReason_ = reason; 907 } 908 909 /++ 910 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. 911 912 History: 913 Added November 25, 2021 (dub v10.4) 914 See_Also: [enabled], [disabledReason] 915 +/ 916 Widget disabledBy() { 917 Widget p = this; 918 while(p) { 919 if(!p._enabled) 920 return p; 921 p = p.parent; 922 } 923 return null; 924 } 925 926 /// Implementations of [ReflectableProperties] interface. See the interface for details. 927 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 928 if(valueIsJson) 929 return SetPropertyResult.wrongFormat; 930 switch(name) { 931 case "name": 932 this.name = value.idup; 933 return SetPropertyResult.success; 934 case "statusTip": 935 this.statusTip = value.idup; 936 return SetPropertyResult.success; 937 default: 938 return SetPropertyResult.noSuchProperty; 939 } 940 } 941 /// ditto 942 void getPropertiesList(scope void delegate(string name) sink) const { 943 sink("name"); 944 sink("statusTip"); 945 } 946 /// ditto 947 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 948 switch(name) { 949 case "name": 950 sink(name, this.name, false); 951 return; 952 case "statusTip": 953 sink(name, this.statusTip, false); 954 return; 955 default: 956 sink(name, null, true); 957 } 958 } 959 960 /++ 961 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 962 963 History: 964 Added November 25, 2021 (dub v10.5) 965 `Point` overload added January 12, 2022 (dub v10.6) 966 +/ 967 int scaleWithDpi(int value, int assumedDpi = 96) { 968 // avoid potential overflow with common special values 969 if(value == int.max) 970 return int.max; 971 if(value == int.min) 972 return int.min; 973 if(value == 0) 974 return 0; 975 return value * currentDpi(assumedDpi) / assumedDpi; 976 } 977 978 /// ditto 979 Point scaleWithDpi(Point value, int assumedDpi = 96) { 980 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 981 } 982 983 /++ 984 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. 985 986 Not entirely stable. 987 988 History: 989 Added August 25, 2023 (dub v11.1) 990 +/ 991 final int currentDpi(int assumedDpi = 96) { 992 // assert(parentWindow !is null); 993 // assert(parentWindow.win !is null); 994 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 995 //divide = 138; // to test 1.5x 996 // 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. 997 // this also covers the case when actualDpi returns 0. 998 if(divide < 96) 999 divide = 96; 1000 return divide; 1001 } 1002 1003 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 1004 // I'll think up something better eventually 1005 1006 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 1007 protected final int defaultLineHeight() { 1008 auto cs = getComputedStyle(); 1009 if(cs.font && !cs.font.isNull) 1010 return castFnumToCnum(cs.font.height() * 5 / 4); 1011 else 1012 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 1013 } 1014 1015 /++ 1016 1017 History: 1018 Added August 25, 2023 (dub v11.1) 1019 +/ 1020 protected final int defaultTextHeight(int numberOfLines = 1) { 1021 auto cs = getComputedStyle(); 1022 if(cs.font && !cs.font.isNull) 1023 return castFnumToCnum(cs.font.height() * numberOfLines); 1024 else 1025 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 1026 } 1027 1028 protected final int defaultTextWidth(const(char)[] text) { 1029 auto cs = getComputedStyle(); 1030 if(cs.font && !cs.font.isNull) 1031 return castFnumToCnum(cs.font.stringWidth(text)); 1032 else 1033 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 1034 } 1035 1036 /++ 1037 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. 1038 1039 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. 1040 1041 History: 1042 Added May 22, 2021 1043 +/ 1044 protected bool encapsulatedChildren() { 1045 return false; 1046 } 1047 1048 private void privateDpiChanged() { 1049 dpiChanged(); 1050 foreach(child; children) 1051 child.privateDpiChanged(); 1052 } 1053 1054 /++ 1055 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 1056 1057 History: 1058 Added January 12, 2022 (dub v10.6) 1059 +/ 1060 protected void dpiChanged() { 1061 1062 } 1063 1064 // Default layout properties { 1065 1066 int minWidth() { return 0; } 1067 int minHeight() { 1068 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 1069 int sum = this.paddingTop + this.paddingBottom; 1070 foreach(child; children) { 1071 if(child.hidden) 1072 continue; 1073 sum += child.minHeight(); 1074 sum += child.marginTop(); 1075 sum += child.marginBottom(); 1076 } 1077 1078 return sum; 1079 } 1080 int maxWidth() { return int.max; } 1081 int maxHeight() { return int.max; } 1082 int widthStretchiness() { return 4; } 1083 int heightStretchiness() { return 4; } 1084 1085 /++ 1086 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 1087 1088 History: 1089 Added June 15, 2021 (dub v10.1) 1090 +/ 1091 int widthShrinkiness() { return 0; } 1092 /// ditto 1093 int heightShrinkiness() { return 0; } 1094 1095 /++ 1096 The initial size of the widget for layout calculations. Default is 0. 1097 1098 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 1099 1100 History: 1101 Added June 15, 2021 (dub v10.1) 1102 +/ 1103 int flexBasisWidth() { return 0; } 1104 /// ditto 1105 int flexBasisHeight() { return 0; } 1106 1107 /++ 1108 Not stable. 1109 1110 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 1111 1112 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 1113 1114 History: 1115 Added January 5, 2023 1116 +/ 1117 Rectangle defaultMargin; 1118 /// ditto 1119 Rectangle defaultPadding; 1120 1121 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 1122 int marginRight() { return scaleWithDpi(defaultMargin.right); } 1123 int marginTop() { return scaleWithDpi(defaultMargin.top); } 1124 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 1125 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 1126 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 1127 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 1128 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 1129 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 1130 1131 private bool recomputeChildLayoutRequired = true; 1132 private static class RecomputeEvent {} 1133 private __gshared rce = new RecomputeEvent(); 1134 protected final void queueRecomputeChildLayout() { 1135 recomputeChildLayoutRequired = true; 1136 1137 if(this.parentWindow) { 1138 auto sw = this.parentWindow.win; 1139 assert(sw !is null); 1140 if(!sw.eventQueued!RecomputeEvent) { 1141 sw.postEvent(rce); 1142 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1143 } 1144 } 1145 1146 } 1147 1148 protected final void recomputeChildLayoutEntry() { 1149 if(recomputeChildLayoutRequired) { 1150 recomputeChildLayout(); 1151 recomputeChildLayoutRequired = false; 1152 redraw(); 1153 } else { 1154 // I still need to check the tree just in case one of them was queued up 1155 // and the event came up here instead of there. 1156 foreach(child; children) 1157 child.recomputeChildLayoutEntry(); 1158 } 1159 } 1160 1161 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 1162 void recomputeChildLayout() { 1163 .recomputeChildLayout!"height"(this); 1164 } 1165 1166 // } 1167 1168 1169 /++ 1170 Returns the style's tag name string this object uses. 1171 1172 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. 1173 1174 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 1175 1176 History: 1177 Added May 10, 2021 1178 +/ 1179 string styleTagName() const { 1180 string n = typeid(this).name; 1181 foreach_reverse(idx, ch; n) 1182 if(ch == '.') { 1183 n = n[idx + 1 .. $]; 1184 break; 1185 } 1186 return n; 1187 } 1188 1189 /// API for the [styleClassList] 1190 static struct ClassList { 1191 private Widget widget; 1192 1193 /// 1194 void add(string s) { 1195 widget.styleClassList_ ~= s; 1196 } 1197 1198 /// 1199 void remove(string s) { 1200 foreach(idx, s1; widget.styleClassList_) 1201 if(s1 == s) { 1202 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 1203 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 1204 widget.styleClassList_.assumeSafeAppend(); 1205 return; 1206 } 1207 } 1208 1209 /// Returns true if it was added, false if it was removed. 1210 bool toggle(string s) { 1211 if(contains(s)) { 1212 remove(s); 1213 return false; 1214 } else { 1215 add(s); 1216 return true; 1217 } 1218 } 1219 1220 /// 1221 bool contains(string s) const { 1222 foreach(s1; widget.styleClassList_) 1223 if(s1 == s) 1224 return true; 1225 return false; 1226 1227 } 1228 } 1229 1230 private string[] styleClassList_; 1231 1232 /++ 1233 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. 1234 1235 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 1236 1237 History: 1238 Added May 10, 2021 1239 +/ 1240 inout(ClassList) styleClassList() inout { 1241 return cast(inout(ClassList)) ClassList(cast() this); 1242 } 1243 1244 /++ 1245 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. 1246 1247 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. 1248 1249 The upper 32 bits are available for your own extensions. 1250 1251 History: 1252 Added May 10, 2021 1253 1254 Examples: 1255 1256 --- 1257 addEventListener((MouseUpEvent ev) { 1258 if(ev.button == MouseButton.left) { 1259 // the first arg is the state to modify, the second arg is what to set it to 1260 setDynamicState(DynamicState.depressed, false); 1261 } 1262 }); 1263 --- 1264 1265 +/ 1266 enum DynamicState : ulong { 1267 focus = (1 << 0), /// the widget currently has the keyboard focus 1268 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 1269 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if no validation has been performed!) 1270 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if no validation has been performed!) 1271 checked = (1 << 4), /// the widget is toggleable and currently toggled on 1272 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 1273 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 1274 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 1275 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. 1276 1277 USER_BEGIN = (1UL << 32), 1278 } 1279 1280 // I want to add the primary and cancel styles to buttons at least at some point somehow. 1281 1282 /// ditto 1283 @property ulong dynamicState() { return dynamicState_; } 1284 /// ditto 1285 @property ulong dynamicState(ulong newValue) { 1286 if(dynamicState != newValue) { 1287 auto old = dynamicState_; 1288 dynamicState_ = newValue; 1289 1290 useStyleProperties((scope Widget.Style s) { 1291 if(s.variesWithState(old ^ newValue)) 1292 redraw(); 1293 }); 1294 } 1295 return dynamicState_; 1296 } 1297 1298 /// ditto 1299 void setDynamicState(ulong flags, bool state) { 1300 auto ds = dynamicState_; 1301 if(state) 1302 ds |= flags; 1303 else 1304 ds &= ~flags; 1305 1306 dynamicState = ds; 1307 } 1308 1309 private ulong dynamicState_; 1310 1311 deprecated("Use dynamic styles instead now") { 1312 Color backgroundColor() { return backgroundColor_; } 1313 void backgroundColor(Color c){ this.backgroundColor_ = c; } 1314 1315 MouseCursor cursor() { return GenericCursor.Default; } 1316 } private Color backgroundColor_ = Color.transparent; 1317 1318 1319 /++ 1320 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). 1321 1322 It is here so there can be a specificity switch. 1323 1324 See [OverrideStyle] for a helper function to use your own. 1325 1326 History: 1327 Added May 11, 2021 1328 +/ 1329 static class Style/* : StyleProperties*/ { 1330 public Widget widget; // public because the mixin template needs access to it 1331 1332 /++ 1333 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 1334 1335 History: 1336 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. 1337 +/ 1338 bool variesWithState(ulong dynamicStateFlags) { 1339 version(win32_widgets) { 1340 if(widget.hwnd) 1341 return false; 1342 } 1343 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 1344 } 1345 1346 /// 1347 Color foregroundColor() { 1348 return WidgetPainter.visualTheme.foregroundColor; 1349 } 1350 1351 /// 1352 WidgetBackground background() { 1353 // the default is a "transparent" background, which means 1354 // it goes as far up as it can to get the color 1355 if (widget.backgroundColor_ != Color.transparent) 1356 return WidgetBackground(widget.backgroundColor_); 1357 if (widget.parent) 1358 return widget.parent.getComputedStyle.background; 1359 return WidgetBackground(widget.backgroundColor_); 1360 } 1361 1362 private static OperatingSystemFont fontCached_; 1363 private OperatingSystemFont fontCached() { 1364 if(fontCached_ is null) 1365 fontCached_ = font(); 1366 return fontCached_; 1367 } 1368 1369 /++ 1370 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. 1371 +/ 1372 OperatingSystemFont font() { 1373 return null; 1374 } 1375 1376 /++ 1377 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. 1378 1379 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1380 1381 History: 1382 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1383 +/ 1384 MouseCursor cursor() { 1385 return GenericCursor.Default; 1386 } 1387 1388 FrameStyle borderStyle() { 1389 return FrameStyle.none; 1390 } 1391 1392 /++ 1393 +/ 1394 Color borderColor() { 1395 return Color.transparent; 1396 } 1397 1398 FrameStyle outlineStyle() { 1399 if(widget.dynamicState & DynamicState.focus) 1400 return FrameStyle.dotted; 1401 else 1402 return FrameStyle.none; 1403 } 1404 1405 Color outlineColor() { 1406 return foregroundColor; 1407 } 1408 } 1409 1410 /++ 1411 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1412 The basic usage is simple: 1413 1414 --- 1415 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1416 // override style hints as-needed here 1417 } 1418 OverrideStyle!Style; // add the method 1419 --- 1420 1421 $(TIP 1422 While the class is not forced to be `static`, for best results, it should be. A non-static class 1423 can not be inherited by other objects whereas the static one can. A property on the base class, 1424 called [Widget.Style.widget|widget], is available for you to access its properties. 1425 ) 1426 1427 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1428 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1429 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1430 1431 1432 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1433 You may also just override `variesWithState` when you use this flag. 1434 1435 --- 1436 mixin OverrideStyle!( 1437 DynamicState.focus, YourFocusedStyle, 1438 DynamicState.hover, YourHoverStyle, 1439 YourDefaultStyle 1440 ) 1441 --- 1442 1443 It checks if `dynamicState` matches the state and if so, returns the object given. 1444 1445 If there is no state mask given, the next one matches everything. The first match given is used. 1446 1447 However, since in most cases you'll want check state inside your individual methods, you probably won't 1448 find much use for this whole-class swap out. 1449 1450 History: 1451 Added May 16, 2021 1452 +/ 1453 static protected mixin template OverrideStyle(S...) { 1454 static import amg = arsd.minigui; 1455 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1456 ulong mask = 0; 1457 foreach(idx, thing; S) { 1458 static if(is(typeof(thing) : ulong)) { 1459 mask = thing; 1460 } else { 1461 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1462 //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."); 1463 scope amg.Widget.Style s = new thing(); 1464 s.widget = this; 1465 dg(s); 1466 return; 1467 } 1468 } 1469 } 1470 } 1471 } 1472 /++ 1473 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1474 +/ 1475 void useStyleProperties(scope void delegate(scope Style props) dg) { 1476 scope Style s = new Style(); 1477 s.widget = this; 1478 dg(s); 1479 } 1480 1481 1482 protected void sendResizeEvent() { 1483 this.emit!ResizeEvent(); 1484 } 1485 1486 /++ 1487 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. 1488 1489 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);` 1490 1491 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. 1492 1493 See_Also: 1494 [createContextMenuFromAnnotatedCode] 1495 +/ 1496 Menu contextMenu(int x, int y) { return null; } 1497 1498 /++ 1499 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, you can pass one as `menuToShow`, but if you don't, it will call [contextMenu], which you can override on a per-widget basis. 1500 1501 History: 1502 The `menuToShow` parameter was added on March 19, 2025. 1503 +/ 1504 final bool showContextMenu(int x, int y, Menu menuToShow = null) { 1505 return showContextMenu(x, y, -2, -2, menuToShow); 1506 } 1507 1508 private final bool showContextMenu(int x, int y, int screenX, int screenY, Menu menu = null) { 1509 if(parentWindow is null || parentWindow.win is null) return false; 1510 1511 if(menu is null) 1512 menu = this.contextMenu(x, y); 1513 1514 if(menu is null) 1515 return false; 1516 1517 version(win32_widgets) { 1518 // FIXME: if it is -1, -1, do it at the current selection location instead 1519 // tho the corner of the window, which it does now, isn't the literal worst. 1520 1521 // i see notepad just seems to put it in the center of the window so idk 1522 1523 if(screenX < 0 && screenY < 0) { 1524 auto p = this.globalCoordinates(); 1525 if(screenX == -2) 1526 p.x += x; 1527 if(screenY == -2) 1528 p.y += y; 1529 1530 screenX = p.x; 1531 screenY = p.y; 1532 } 1533 1534 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1535 throw new Exception("TrackContextMenuEx"); 1536 } else version(custom_widgets) { 1537 menu.popup(this, x, y); 1538 } 1539 1540 return true; 1541 } 1542 1543 /++ 1544 Removes this widget from its parent. 1545 1546 History: 1547 `removeWidget` was made `final` on May 11, 2021. 1548 +/ 1549 @scriptable 1550 final void removeWidget() { 1551 auto p = this.parent; 1552 if(p) { 1553 int item; 1554 for(item = 0; item < p._children.length; item++) 1555 if(p._children[item] is this) 1556 break; 1557 auto idx = item; 1558 for(; item < p._children.length - 1; item++) 1559 p._children[item] = p._children[item + 1]; 1560 p._children = p._children[0 .. $-1]; 1561 1562 this.parent.widgetRemoved(idx, this); 1563 //this.parent = null; 1564 1565 p.queueRecomputeChildLayout(); 1566 } 1567 version(win32_widgets) { 1568 removeAllChildren(); 1569 if(hwnd) { 1570 DestroyWindow(hwnd); 1571 hwnd = null; 1572 } 1573 } 1574 } 1575 1576 /++ 1577 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. 1578 1579 History: 1580 Added September 19, 2021 1581 +/ 1582 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1583 1584 /++ 1585 Removes all child widgets from `this`. You should not use the removed widgets again. 1586 1587 Note that on Windows, it also destroys the native handles for the removed children recursively. 1588 1589 History: 1590 Added July 1, 2021 (dub v10.2) 1591 +/ 1592 void removeAllChildren() { 1593 version(win32_widgets) 1594 foreach(child; _children) { 1595 child.removeAllChildren(); 1596 if(child.hwnd) { 1597 DestroyWindow(child.hwnd); 1598 child.hwnd = null; 1599 } 1600 } 1601 auto orig = this._children; 1602 this._children = null; 1603 foreach(idx, w; orig) 1604 this.widgetRemoved(idx, w); 1605 1606 queueRecomputeChildLayout(); 1607 } 1608 1609 /++ 1610 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1611 +/ 1612 @scriptable 1613 Widget getChildByName(string name) { 1614 return getByName(name); 1615 } 1616 /++ 1617 Finds the nearest descendant with the requested type and [name]. May return `this`. 1618 +/ 1619 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1620 if(this.name == name) 1621 if(auto c = cast(WidgetClass) this) 1622 return c; 1623 foreach(child; children) { 1624 auto w = child.getByName(name); 1625 if(auto c = cast(WidgetClass) w) 1626 return c; 1627 } 1628 return null; 1629 } 1630 1631 /++ 1632 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. 1633 Names should be unique in a window. 1634 1635 See_Also: [getByName], [getChildByName] 1636 +/ 1637 @scriptable string name; 1638 1639 private EventHandler[][string] bubblingEventHandlers; 1640 private EventHandler[][string] capturingEventHandlers; 1641 1642 /++ 1643 Default event handlers. These are called on the appropriate 1644 event unless [Event.preventDefault] is called on the event at 1645 some point through the bubbling process. 1646 1647 1648 If you are implementing your own widget and want to add custom 1649 events, you should follow the same pattern here: create a virtual 1650 function named `defaultEventHandler_eventname` with the implementation, 1651 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1652 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1653 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1654 This ensures virtual dispatch based on the correct subclass. 1655 1656 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1657 overridden version. 1658 1659 You only need to do that on parent classes adding NEW event types. If you 1660 just want to change the default behavior of an existing event type in a subclass, 1661 you override the function (and optionally call `super.method_name`) like normal. 1662 1663 History: 1664 Some of the events changed to take specific subclasses instead of generic `Event` 1665 on January 3, 2025. 1666 1667 +/ 1668 protected EventHandler[string] defaultEventHandlers; 1669 1670 /// ditto 1671 void setupDefaultEventHandlers() { 1672 defaultEventHandlers["click"] = (Widget t, Event event) { if(auto e = cast(ClickEvent) event) t.defaultEventHandler_click(e); }; 1673 defaultEventHandlers["dblclick"] = (Widget t, Event event) { if(auto e = cast(DoubleClickEvent) event) t.defaultEventHandler_dblclick(e); }; 1674 defaultEventHandlers["keydown"] = (Widget t, Event event) { if(auto e = cast(KeyDownEvent) event) t.defaultEventHandler_keydown(e); }; 1675 defaultEventHandlers["keyup"] = (Widget t, Event event) { if(auto e = cast(KeyUpEvent) event) t.defaultEventHandler_keyup(e); }; 1676 defaultEventHandlers["mouseover"] = (Widget t, Event event) { if(auto e = cast(MouseOverEvent) event) t.defaultEventHandler_mouseover(e); }; 1677 defaultEventHandlers["mouseout"] = (Widget t, Event event) { if(auto e = cast(MouseOutEvent) event) t.defaultEventHandler_mouseout(e); }; 1678 defaultEventHandlers["mousedown"] = (Widget t, Event event) { if(auto e = cast(MouseDownEvent) event) t.defaultEventHandler_mousedown(e); }; 1679 defaultEventHandlers["mouseup"] = (Widget t, Event event) { if(auto e = cast(MouseUpEvent) event) t.defaultEventHandler_mouseup(e); }; 1680 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { if(auto e = cast(MouseEnterEvent) event) t.defaultEventHandler_mouseenter(e); }; 1681 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { if(auto e = cast(MouseLeaveEvent) event) t.defaultEventHandler_mouseleave(e); }; 1682 defaultEventHandlers["mousemove"] = (Widget t, Event event) { if(auto e = cast(MouseMoveEvent) event) t.defaultEventHandler_mousemove(e); }; 1683 defaultEventHandlers["char"] = (Widget t, Event event) { if(auto e = cast(CharEvent) event) t.defaultEventHandler_char(e); }; 1684 defaultEventHandlers["triggered"] = (Widget t, Event event) { if(auto e = cast(Event) event) t.defaultEventHandler_triggered(e); }; 1685 defaultEventHandlers["change"] = (Widget t, Event event) { if(auto e = cast(ChangeEventBase) event) t.defaultEventHandler_change(e); }; 1686 defaultEventHandlers["focus"] = (Widget t, Event event) { if(auto e = cast(FocusEvent) event) t.defaultEventHandler_focus(e); }; 1687 defaultEventHandlers["blur"] = (Widget t, Event event) { if(auto e = cast(BlurEvent) event) t.defaultEventHandler_blur(e); }; 1688 defaultEventHandlers["focusin"] = (Widget t, Event event) { if(auto e = cast(FocusInEvent) event) t.defaultEventHandler_focusin(e); }; 1689 defaultEventHandlers["focusout"] = (Widget t, Event event) { if(auto e = cast(FocusOutEvent) event) t.defaultEventHandler_focusout(e); }; 1690 } 1691 1692 /// ditto 1693 void defaultEventHandler_click(ClickEvent event) {} 1694 /// ditto 1695 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1696 /// ditto 1697 void defaultEventHandler_keydown(KeyDownEvent event) {} 1698 /// ditto 1699 void defaultEventHandler_keyup(KeyUpEvent event) {} 1700 /// ditto 1701 void defaultEventHandler_mousedown(MouseDownEvent event) { 1702 if(event.button == MouseButton.left) { 1703 if(this.tabStop) { 1704 this.focus(); 1705 } 1706 } else if(event.button == MouseButton.right) { 1707 showContextMenu(event.clientX, event.clientY); 1708 } 1709 } 1710 /// ditto 1711 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1712 /// ditto 1713 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1714 /// ditto 1715 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1716 /// ditto 1717 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1718 /// ditto 1719 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1720 /// ditto 1721 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1722 /// ditto 1723 void defaultEventHandler_char(CharEvent event) {} 1724 /// ditto 1725 void defaultEventHandler_triggered(Event event) {} 1726 /// ditto 1727 void defaultEventHandler_change(ChangeEventBase event) {} 1728 /// ditto 1729 void defaultEventHandler_focus(FocusEvent event) {} 1730 /// ditto 1731 void defaultEventHandler_blur(BlurEvent event) {} 1732 /// ditto 1733 void defaultEventHandler_focusin(FocusInEvent event) {} 1734 /// ditto 1735 void defaultEventHandler_focusout(FocusOutEvent event) {} 1736 1737 /++ 1738 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1739 1740 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1741 1742 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1743 of participating in handler delegation. 1744 1745 $(TIP 1746 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. 1747 ) 1748 +/ 1749 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1750 return addEventListener(event, (Widget, scope Event e) { 1751 if(e.srcElement is this) 1752 handler(); 1753 }, useCapture); 1754 } 1755 1756 /// ditto 1757 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1758 return addEventListener(event, (Widget, Event e) { 1759 if(e.srcElement is this) 1760 handler(e); 1761 }, useCapture); 1762 } 1763 1764 /// ditto 1765 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1766 static if(is(Handler Fn == delegate)) { 1767 static if(is(Fn Params == __parameters)) { 1768 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1769 if(e.srcElement !is this) 1770 return; 1771 auto ty = cast(Params[0]) e; 1772 if(ty !is null) 1773 handler(ty); 1774 }, useCapture); 1775 } else static assert(0); 1776 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1777 } 1778 1779 /// ditto 1780 @scriptable 1781 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1782 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1783 } 1784 1785 /// ditto 1786 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1787 static if(is(Handler Fn == delegate)) { 1788 static if(is(Fn Params == __parameters)) { 1789 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1790 auto ty = cast(Params[0]) e; 1791 if(ty !is null) 1792 handler(ty); 1793 }, useCapture); 1794 } else static assert(0); 1795 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1796 } 1797 1798 /// ditto 1799 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1800 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1801 } 1802 1803 /// ditto 1804 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1805 if(event.length > 2 && event[0..2] == "on") 1806 event = event[2 .. $]; 1807 1808 if(useCapture) 1809 capturingEventHandlers[event] ~= handler; 1810 else 1811 bubblingEventHandlers[event] ~= handler; 1812 1813 return EventListener(this, event, handler, useCapture); 1814 } 1815 1816 /// ditto 1817 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1818 if(event.length > 2 && event[0..2] == "on") 1819 event = event[2 .. $]; 1820 1821 if(useCapture) { 1822 if(event in capturingEventHandlers) 1823 foreach(ref evt; capturingEventHandlers[event]) 1824 if(evt is handler) evt = null; 1825 } else { 1826 if(event in bubblingEventHandlers) 1827 foreach(ref evt; bubblingEventHandlers[event]) 1828 if(evt is handler) evt = null; 1829 } 1830 } 1831 1832 /// ditto 1833 void removeEventListener(EventListener listener) { 1834 removeEventListener(listener.event, listener.handler, listener.useCapture); 1835 } 1836 1837 static if(UsingSimpledisplayX11) { 1838 void discardXConnectionState() { 1839 foreach(child; children) 1840 child.discardXConnectionState(); 1841 } 1842 1843 void recreateXConnectionState() { 1844 foreach(child; children) 1845 child.recreateXConnectionState(); 1846 redraw(); 1847 } 1848 } 1849 1850 /++ 1851 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1852 1853 History: 1854 `globalCoordinates` was made `final` on May 11, 2021. 1855 +/ 1856 Point globalCoordinates() { 1857 int x = this.x; 1858 int y = this.y; 1859 auto p = this.parent; 1860 while(p) { 1861 x += p.x; 1862 y += p.y; 1863 p = p.parent; 1864 } 1865 1866 static if(UsingSimpledisplayX11) { 1867 auto dpy = XDisplayConnection.get; 1868 arsd.simpledisplay.Window dummyw; 1869 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1870 } else version(Windows) { 1871 POINT pt; 1872 pt.x = x; 1873 pt.y = y; 1874 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1875 x = pt.x; 1876 y = pt.y; 1877 } else { 1878 auto rect = this.parentWindow.win.impl.window.frame; 1879 // FIXME: confirm? 1880 x += cast(int) rect.origin.x; 1881 y += cast(int) rect.origin.y; 1882 } 1883 1884 return Point(x, y); 1885 } 1886 1887 version(win32_widgets) 1888 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1889 1890 version(win32_widgets) 1891 /// Called when a WM_COMMAND is sent to the associated hwnd. 1892 void handleWmCommand(ushort cmd, ushort id) {} 1893 1894 version(win32_widgets) 1895 /++ 1896 Called when a WM_NOTIFY is sent to the associated hwnd. 1897 1898 History: 1899 +/ 1900 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1901 1902 version(win32_widgets) 1903 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); } 1904 1905 /++ 1906 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1907 1908 Updates to this variable will only be made visible on the next mouse enter event. 1909 +/ 1910 @scriptable string statusTip; 1911 // string toolTip; 1912 // string helpText; 1913 1914 /++ 1915 If true, this widget can be focused via keyboard control with the tab key. 1916 1917 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1918 +/ 1919 bool tabStop = true; 1920 /++ 1921 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.) 1922 +/ 1923 int tabOrder; 1924 1925 version(win32_widgets) { 1926 static Widget[HWND] nativeMapping; 1927 /// The native handle, if there is one. 1928 HWND hwnd; 1929 WNDPROC originalWindowProcedure; 1930 1931 SimpleWindow simpleWindowWrappingHwnd; 1932 1933 // please note it IGNORES your return value and does NOT forward it to Windows! 1934 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1935 return 0; 1936 } 1937 } 1938 private bool implicitlyCreated; 1939 1940 /// 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. 1941 int x; 1942 /// ditto 1943 int y; 1944 private int _width; 1945 private int _height; 1946 private Widget[] _children; 1947 private Widget _parent; 1948 private Window _parentWindow; 1949 1950 /++ 1951 Returns the window to which this widget is attached. 1952 1953 History: 1954 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. 1955 +/ 1956 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1957 private @property void parentWindow(Window parent) { 1958 auto old = _parentWindow; 1959 _parentWindow = parent; 1960 newParentWindow(old, _parentWindow); 1961 foreach(child; children) 1962 child.parentWindow = parent; // please note that this is recursive 1963 } 1964 1965 /++ 1966 Called when the widget has been added to or remove from a parent window. 1967 1968 Note that either oldParent and/or newParent may be null any time this is called. 1969 1970 History: 1971 Added September 13, 2024 1972 +/ 1973 protected void newParentWindow(Window oldParent, Window newParent) {} 1974 1975 /++ 1976 Returns the list of the widget's children. 1977 1978 History: 1979 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1980 1981 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1982 +/ 1983 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1984 1985 /++ 1986 Returns the widget's parent. 1987 1988 History: 1989 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1990 1991 The parent should only be managed by the [addChild] and [removeWidget] method. 1992 +/ 1993 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1994 1995 /// The widget's current size. 1996 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1997 /// ditto 1998 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1999 2000 /// Only the layout manager should be calling these. 2001 final protected @property int width(int a) @safe { return _width = a; } 2002 /// ditto 2003 final protected @property int height(int a) @safe { return _height = a; } 2004 2005 /++ 2006 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. 2007 2008 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 2009 +/ 2010 protected void registerMovement() { 2011 version(win32_widgets) { 2012 if(hwnd) { 2013 auto pos = getChildPositionRelativeToParentHwnd(this); 2014 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 2015 this.redraw(); 2016 } 2017 } 2018 sendResizeEvent(); 2019 } 2020 2021 /// Creates the widget and adds it to the parent. 2022 this(Widget parent) { 2023 if(parent !is null) 2024 parent.addChild(this); 2025 setupDefaultEventHandlers(); 2026 } 2027 2028 /// 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. 2029 @scriptable 2030 bool isFocused() { 2031 return parentWindow && parentWindow.focusedWidget is this; 2032 } 2033 2034 private bool showing_ = true; 2035 /// 2036 bool showing() const { return showing_; } 2037 /// 2038 bool hidden() const { return !showing_; } 2039 /++ 2040 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. 2041 2042 Note that a widget only ever shows if all its parents are showing too. 2043 +/ 2044 void showing(bool s, bool recalculate = true) { 2045 if(s != showing_) { 2046 showing_ = s; 2047 // writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s); 2048 2049 showNativeWindowChildren(s); 2050 2051 if(parent && recalculate) { 2052 parent.queueRecomputeChildLayout(); 2053 parent.redraw(); 2054 } 2055 2056 if(s) { 2057 queueRecomputeChildLayout(); 2058 redraw(); 2059 } 2060 } 2061 } 2062 /// Convenience method for `showing = true` 2063 @scriptable 2064 void show() { 2065 showing = true; 2066 } 2067 /// Convenience method for `showing = false` 2068 @scriptable 2069 void hide() { 2070 showing = false; 2071 } 2072 2073 /++ 2074 If you are a native window, show/hide it based on shouldShow and return `true`. 2075 2076 Otherwise, do nothing and return false. 2077 +/ 2078 protected bool showOrHideIfNativeWindow(bool shouldShow) { 2079 version(win32_widgets) { 2080 if(hwnd) { 2081 ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE); 2082 return true; 2083 } else { 2084 return false; 2085 } 2086 } else { 2087 return false; 2088 } 2089 } 2090 2091 private void showNativeWindowChildren(bool s) { 2092 if(!showOrHideIfNativeWindow(s && showing)) 2093 foreach(child; children) 2094 child.showNativeWindowChildren(s); 2095 } 2096 2097 /// 2098 @scriptable 2099 void focus() { 2100 assert(parentWindow !is null); 2101 if(isFocused()) 2102 return; 2103 2104 if(parentWindow.focusedWidget) { 2105 // FIXME: more details here? like from and to 2106 auto from = parentWindow.focusedWidget; 2107 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 2108 parentWindow.focusedWidget = null; 2109 from.emit!BlurEvent(); 2110 from.emit!FocusOutEvent(); 2111 } 2112 2113 2114 version(win32_widgets) { 2115 if(this.hwnd !is null) 2116 SetFocus(this.hwnd); 2117 } 2118 //else static if(UsingSimpledisplayX11) 2119 //this.parentWindow.win.focus(); 2120 2121 parentWindow.focusedWidget = this; 2122 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 2123 this.emit!FocusEvent(); 2124 this.emit!FocusInEvent(); 2125 } 2126 2127 /+ 2128 /++ 2129 Unfocuses the widget. This may reset 2130 +/ 2131 @scriptable 2132 void blur() { 2133 2134 } 2135 +/ 2136 2137 2138 /++ 2139 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 2140 2141 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 2142 +/ 2143 void attachedToWindow(Window w) {} 2144 /++ 2145 Callback when the widget is added to another widget. 2146 2147 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 2148 +/ 2149 void addedTo(Widget w) {} 2150 2151 /++ 2152 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. 2153 2154 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 2155 +/ 2156 protected void addChild(Widget w, int position = int.max) { 2157 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 2158 assert(w !is this, "Child cannot be its own parent!"); 2159 w._parent = this; 2160 if(position == int.max || position == children.length) { 2161 _children ~= w; 2162 } else { 2163 assert(position < _children.length); 2164 _children.length = _children.length + 1; 2165 for(int i = cast(int) _children.length - 1; i > position; i--) 2166 _children[i] = _children[i - 1]; 2167 _children[position] = w; 2168 } 2169 2170 this.parentWindow = this._parentWindow; 2171 2172 w.addedTo(this); 2173 2174 bool parentIsNative; 2175 version(win32_widgets) { 2176 parentIsNative = hwnd !is null; 2177 } 2178 if(!parentIsNative && !showing) 2179 w.showOrHideIfNativeWindow(false); 2180 2181 if(parentWindow !is null) { 2182 w.attachedToWindow(parentWindow); 2183 parentWindow.queueRecomputeChildLayout(); 2184 parentWindow.redraw(); 2185 } 2186 } 2187 2188 /++ 2189 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. 2190 +/ 2191 Widget getChildAtPosition(int x, int y) { 2192 // it goes backward so the last one to show gets picked first 2193 // might use z-index later 2194 foreach_reverse(child; children) { 2195 if(child.hidden) 2196 continue; 2197 if(child.x <= x && child.y <= y 2198 && ((x - child.x) < child.width) 2199 && ((y - child.y) < child.height)) 2200 { 2201 return child; 2202 } 2203 } 2204 2205 return null; 2206 } 2207 2208 /++ 2209 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. 2210 2211 History: 2212 Added July 2, 2021 (v10.2) 2213 +/ 2214 protected void addScrollPosition(ref int x, ref int y) {} 2215 2216 /++ 2217 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. 2218 2219 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. 2220 2221 [paint] is not called for system widgets as the OS library draws them instead. 2222 2223 2224 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. 2225 2226 You should also look at [WidgetPainter.visualTheme] to be theme aware. 2227 2228 History: 2229 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. 2230 +/ 2231 void paint(WidgetPainter painter) { 2232 version(win32_widgets) 2233 if(hwnd) { 2234 return; 2235 } 2236 painter.drawThemed(&paintContent); // note this refers to the following overload 2237 } 2238 2239 /++ 2240 Responsible for drawing the content as the theme engine is responsible for other elements. 2241 2242 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 2243 2244 Params: 2245 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. 2246 2247 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. 2248 2249 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. 2250 2251 Returns: 2252 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. 2253 2254 History: 2255 Added May 15, 2021 2256 +/ 2257 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2258 return bounds; 2259 } 2260 2261 deprecated("Change ScreenPainter to WidgetPainter") 2262 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 2263 2264 /// I don't actually like the name of this 2265 /// this draws a background on it 2266 void erase(WidgetPainter painter) { 2267 version(win32_widgets) 2268 if(hwnd) return; // Windows will do it. I think. 2269 2270 auto c = getComputedStyle().background.color; 2271 painter.fillColor = c; 2272 painter.outlineColor = c; 2273 2274 version(win32_widgets) { 2275 HANDLE b, p; 2276 if(c.a == 0 && parent is parentWindow) { 2277 // I don't remember why I had this really... 2278 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 2279 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 2280 } 2281 } 2282 painter.drawRectangle(Point(0, 0), width, height); 2283 version(win32_widgets) { 2284 if(c.a == 0 && parent is parentWindow) { 2285 SelectObject(painter.impl.hdc, p); 2286 SelectObject(painter.impl.hdc, b); 2287 } 2288 } 2289 } 2290 2291 /// 2292 WidgetPainter draw() { 2293 int x = this.x, y = this.y; 2294 auto parent = this.parent; 2295 while(parent) { 2296 x += parent.x; 2297 y += parent.y; 2298 parent = parent.parent; 2299 } 2300 2301 auto painter = parentWindow.win.draw(true); 2302 painter.originX = x; 2303 painter.originY = y; 2304 painter.setClipRectangle(Point(0, 0), width, height); 2305 return WidgetPainter(painter, this); 2306 } 2307 2308 /// 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. 2309 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 2310 if(hidden) 2311 return; 2312 2313 int paintX = x; 2314 int paintY = y; 2315 if(this.useNativeDrawing()) { 2316 paintX = 0; 2317 paintY = 0; 2318 lox = 0; 2319 loy = 0; 2320 containment = Rectangle(0, 0, int.max, int.max); 2321 } 2322 2323 painter.originX = lox + paintX; 2324 painter.originY = loy + paintY; 2325 2326 bool actuallyPainted = false; 2327 2328 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 2329 if(clip == Rectangle.init) { 2330 // writeln(this, " clipped out"); 2331 return; 2332 } 2333 2334 bool invalidateChildren = invalidate; 2335 2336 if(redrawRequested || force) { 2337 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 2338 2339 painter.drawingUpon = this; 2340 2341 erase(painter); 2342 if(painter.visualTheme) 2343 painter.visualTheme.doPaint(this, painter); 2344 else 2345 paint(painter); 2346 2347 if(invalidate) { 2348 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 2349 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 2350 painter.invalidateRect(region); 2351 // children are contained inside this, so no need to do extra work 2352 invalidateChildren = false; 2353 } 2354 2355 redrawRequested = false; 2356 actuallyPainted = true; 2357 } 2358 2359 foreach(child; children) { 2360 version(win32_widgets) 2361 if(child.useNativeDrawing()) continue; 2362 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 2363 } 2364 2365 version(win32_widgets) 2366 foreach(child; children) { 2367 if(child.useNativeDrawing) { 2368 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 2369 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 2370 } 2371 } 2372 } 2373 2374 protected bool useNativeDrawing() nothrow { 2375 version(win32_widgets) 2376 return hwnd !is null; 2377 else 2378 return false; 2379 } 2380 2381 private static class RedrawEvent {} 2382 private __gshared re = new RedrawEvent(); 2383 2384 private bool redrawRequested; 2385 /// 2386 final void redraw(string file = __FILE__, size_t line = __LINE__) { 2387 redrawRequested = true; 2388 2389 if(this.parentWindow) { 2390 auto sw = this.parentWindow.win; 2391 assert(sw !is null); 2392 if(!sw.eventQueued!RedrawEvent) { 2393 sw.postEvent(re); 2394 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 2395 } 2396 } 2397 } 2398 2399 private SimpleWindow drawableWindow; 2400 2401 /++ 2402 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. 2403 2404 Returns: 2405 `true` if you should do your default behavior. 2406 2407 History: 2408 Added May 5, 2021 2409 2410 Bugs: 2411 It does not do the static checks on gdc right now. 2412 +/ 2413 final protected bool emit(EventType, this This, Args...)(Args args) { 2414 version(GNU) {} else 2415 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2416 auto e = new EventType(this, args); 2417 e.dispatch(); 2418 return !e.defaultPrevented; 2419 } 2420 /// ditto 2421 final protected bool emit(string eventString, this This)() { 2422 auto e = new Event(eventString, this); 2423 e.dispatch(); 2424 return !e.defaultPrevented; 2425 } 2426 2427 /++ 2428 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. 2429 2430 History: 2431 Added May 5, 2021 2432 +/ 2433 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 2434 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2435 return addEventListener(handler); 2436 } 2437 2438 /++ 2439 Gets the computed style properties from the visual theme. 2440 2441 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].) 2442 2443 History: 2444 Added May 8, 2021 2445 +/ 2446 final StyleInformation getComputedStyle() { 2447 return StyleInformation(this); 2448 } 2449 2450 int focusableWidgets(scope int delegate(Widget) dg) { 2451 foreach(widget; WidgetStream(this)) { 2452 if(widget.tabStop && !widget.hidden) { 2453 int result = dg(widget); 2454 if (result) 2455 return result; 2456 } 2457 } 2458 return 0; 2459 } 2460 2461 /++ 2462 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2463 for the given content box (the area between the padding) 2464 2465 History: 2466 Added January 4, 2023 (dub v11.0) 2467 +/ 2468 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2469 auto cs = getComputedStyle(); 2470 2471 auto borderWidth = getBorderWidth(cs.borderStyle); 2472 2473 auto rect = contentBox; 2474 2475 rect.left -= borderWidth; 2476 rect.right += borderWidth; 2477 rect.top -= borderWidth; 2478 rect.bottom += borderWidth; 2479 2480 auto insideBorderRect = rect; 2481 2482 rect.left -= cs.paddingLeft; 2483 rect.right += cs.paddingRight; 2484 rect.top -= cs.paddingTop; 2485 rect.bottom += cs.paddingBottom; 2486 2487 return rect; 2488 } 2489 2490 2491 // FIXME: I kinda want to hide events from implementation widgets 2492 // so it just catches them all and stops propagation... 2493 // i guess i can do it with a event listener on star. 2494 2495 mixin Emits!KeyDownEvent; /// 2496 mixin Emits!KeyUpEvent; /// 2497 mixin Emits!CharEvent; /// 2498 2499 mixin Emits!MouseDownEvent; /// 2500 mixin Emits!MouseUpEvent; /// 2501 mixin Emits!ClickEvent; /// 2502 mixin Emits!DoubleClickEvent; /// 2503 mixin Emits!MouseMoveEvent; /// 2504 mixin Emits!MouseOverEvent; /// 2505 mixin Emits!MouseOutEvent; /// 2506 mixin Emits!MouseEnterEvent; /// 2507 mixin Emits!MouseLeaveEvent; /// 2508 2509 mixin Emits!ResizeEvent; /// 2510 2511 mixin Emits!BlurEvent; /// 2512 mixin Emits!FocusEvent; /// 2513 2514 mixin Emits!FocusInEvent; /// 2515 mixin Emits!FocusOutEvent; /// 2516 } 2517 2518 /+ 2519 /++ 2520 Interface to indicate that the widget has a simple value property. 2521 2522 History: 2523 Added August 26, 2021 2524 +/ 2525 interface HasValue!T { 2526 /// Getter 2527 @property T value(); 2528 /// Setter 2529 @property void value(T); 2530 } 2531 2532 /++ 2533 Interface to indicate that the widget has a range of possible values for its simple value property. 2534 This would be present on something like a slider or possibly a number picker. 2535 2536 History: 2537 Added September 11, 2021 2538 +/ 2539 interface HasRangeOfValues!T : HasValue!T { 2540 /// The minimum and maximum values in the range, inclusive. 2541 @property T minValue(); 2542 @property void minValue(T); /// ditto 2543 @property T maxValue(); /// ditto 2544 @property void maxValue(T); /// ditto 2545 2546 /// The smallest step the user interface allows. User may still type in values without this limitation. 2547 @property void step(T); 2548 @property T step(); /// ditto 2549 } 2550 2551 /++ 2552 Interface to indicate that the widget has a list of possible values the user can choose from. 2553 This would be present on something like a drop-down selector. 2554 2555 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2556 combobox. 2557 2558 History: 2559 Added September 11, 2021 2560 +/ 2561 interface HasListOfValues!T : HasValue!T { 2562 @property T[] values; 2563 @property void values(T[]); 2564 2565 @property int selectedIndex(); // note it may return -1! 2566 @property void selectedIndex(int); 2567 } 2568 +/ 2569 2570 /++ 2571 History: 2572 Added September 2021 (dub v10.4) 2573 +/ 2574 class GridLayout : Layout { 2575 2576 // 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. 2577 2578 /++ 2579 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2580 +/ 2581 enum Gravity { 2582 Center = 0, 2583 NorthWest = North | West, 2584 North = 0b10_00, 2585 NorthEast = North | East, 2586 West = 0b00_10, 2587 East = 0b00_01, 2588 SouthWest = South | West, 2589 South = 0b01_00, 2590 SouthEast = South | East, 2591 } 2592 2593 /++ 2594 The width and height are in some proportional units and can often just be 12. 2595 +/ 2596 this(int width, int height, Widget parent) { 2597 this.gridWidth = width; 2598 this.gridHeight = height; 2599 super(parent); 2600 } 2601 2602 /++ 2603 Sets the position of the given child. 2604 2605 The units of these arguments are in the proportional grid units you set in the constructor. 2606 +/ 2607 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2608 // ensure it is in bounds 2609 // then ensure no overlaps 2610 2611 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2612 2613 foreach(ref position; positions) { 2614 if(position.widget is child) { 2615 position = p; 2616 goto set; 2617 } 2618 } 2619 2620 positions ~= p; 2621 2622 set: 2623 2624 // FIXME: should this batch? 2625 queueRecomputeChildLayout(); 2626 2627 return child; 2628 } 2629 2630 override void addChild(Widget w, int position = int.max) { 2631 super.addChild(w, position); 2632 //positions ~= ChildPosition(w); 2633 if(position != int.max) { 2634 // FIXME: align it so they actually match. 2635 } 2636 } 2637 2638 override void widgetRemoved(size_t idx, Widget w) { 2639 // FIXME: keep the positions array aligned 2640 // positions[idx].widget = null; 2641 } 2642 2643 override void recomputeChildLayout() { 2644 registerMovement(); 2645 int onGrid = cast(int) positions.length; 2646 c: foreach(child; children) { 2647 // just snap it to the grid 2648 if(onGrid) 2649 foreach(position; positions) 2650 if(position.widget is child) { 2651 child.x = this.width * position.x / this.gridWidth; 2652 child.y = this.height * position.y / this.gridHeight; 2653 child.width = this.width * position.width / this.gridWidth; 2654 child.height = this.height * position.height / this.gridHeight; 2655 2656 auto diff = child.width - child.maxWidth(); 2657 // FIXME: gravity? 2658 if(diff > 0) { 2659 child.width = child.width - diff; 2660 2661 if(position.gravity & Gravity.West) { 2662 // nothing needed, already aligned 2663 } else if(position.gravity & Gravity.East) { 2664 child.x += diff; 2665 } else { 2666 child.x += diff / 2; 2667 } 2668 } 2669 2670 diff = child.height - child.maxHeight(); 2671 // FIXME: gravity? 2672 if(diff > 0) { 2673 child.height = child.height - diff; 2674 2675 if(position.gravity & Gravity.North) { 2676 // nothing needed, already aligned 2677 } else if(position.gravity & Gravity.South) { 2678 child.y += diff; 2679 } else { 2680 child.y += diff / 2; 2681 } 2682 } 2683 child.recomputeChildLayout(); 2684 onGrid--; 2685 continue c; 2686 } 2687 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2688 } 2689 } 2690 2691 private struct ChildPosition { 2692 Widget widget; 2693 int x; 2694 int y; 2695 int width; 2696 int height; 2697 Gravity gravity; 2698 } 2699 private ChildPosition[] positions; 2700 2701 int gridWidth = 12; 2702 int gridHeight = 12; 2703 } 2704 2705 /// 2706 abstract class ComboboxBase : Widget { 2707 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2708 // or to always show the list, we want CBS_SIMPLE == 1 2709 version(win32_widgets) 2710 this(uint style, Widget parent) { 2711 super(parent); 2712 createWin32Window(this, "ComboBox"w, null, style); 2713 } 2714 else version(custom_widgets) 2715 this(Widget parent) { 2716 super(parent); 2717 2718 addEventListener((KeyDownEvent event) { 2719 if(event.key == Key.Up) { 2720 setSelection(selection_-1); 2721 event.preventDefault(); 2722 } 2723 if(event.key == Key.Down) { 2724 setSelection(selection_+1); 2725 event.preventDefault(); 2726 } 2727 2728 }); 2729 2730 } 2731 else static assert(false); 2732 2733 protected void scrollSelectionIntoView() {} 2734 2735 /++ 2736 Returns the current list of options in the selection. 2737 2738 History: 2739 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2740 +/ 2741 final @property string[] options() const { 2742 return cast(string[]) options_; 2743 } 2744 2745 /++ 2746 Replaces the list of options in the box. Note that calling this will also reset the selection. 2747 2748 History: 2749 Added December, 29 2024 2750 +/ 2751 final @property void options(string[] options) { 2752 version(win32_widgets) 2753 SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0); 2754 selection_ = -1; 2755 options_ = null; 2756 foreach(opt; options) 2757 addOption(opt); 2758 2759 version(custom_widgets) 2760 redraw(); 2761 } 2762 2763 private string[] options_; 2764 private int selection_ = -1; 2765 2766 /++ 2767 Adds an option to the end of options array. 2768 +/ 2769 void addOption(string s) { 2770 options_ ~= s; 2771 version(win32_widgets) 2772 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2773 } 2774 2775 /++ 2776 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2777 +/ 2778 int getSelection() { 2779 return selection_; 2780 } 2781 2782 /++ 2783 Returns the current selection as a string. 2784 2785 History: 2786 Added November 17, 2021 2787 +/ 2788 string getSelectionString() { 2789 return selection_ == -1 ? null : options[selection_]; 2790 } 2791 2792 /++ 2793 Sets the current selection to an index in the options array, or to the given option if present. 2794 Please note that the string version may do a linear lookup. 2795 2796 Returns: 2797 the index you passed in 2798 2799 History: 2800 The `string` based overload was added on March 1, 2022 (dub v10.7). 2801 2802 The return value was `void` prior to March 1, 2022. 2803 +/ 2804 int setSelection(int idx) { 2805 if(idx < -1) 2806 idx = -1; 2807 if(idx + 1 > options.length) 2808 idx = cast(int) options.length - 1; 2809 2810 selection_ = idx; 2811 2812 version(win32_widgets) 2813 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2814 2815 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2816 t.dispatch(); 2817 2818 scrollSelectionIntoView(); 2819 2820 return idx; 2821 } 2822 2823 /// ditto 2824 int setSelection(string s) { 2825 if(s !is null) 2826 foreach(idx, item; options) 2827 if(item == s) { 2828 return setSelection(cast(int) idx); 2829 } 2830 return setSelection(-1); 2831 } 2832 2833 /++ 2834 This event is fired when the selection changes. Both [Event.stringValue] and 2835 [Event.intValue] are filled in - `stringValue` is the text in the selection 2836 and `intValue` is the index of the selection. If the combo box allows multiple 2837 selection, these values will include only one of the selected items - for those, 2838 you should loop through the values and check their selected flag instead. 2839 2840 (I know that sucks, but it is how it is right now.) 2841 2842 History: 2843 It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025. 2844 This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the 2845 base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to 2846 be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there. 2847 +/ 2848 static final class SelectionChangedEvent : ChangeEventBase { 2849 this(Widget target, int iv, string sv) { 2850 super(target); 2851 this.iv = iv; 2852 this.sv = sv; 2853 } 2854 immutable int iv; 2855 immutable string sv; 2856 2857 deprecated("Use stringValue or intValue instead") @property string value() { 2858 return sv; 2859 } 2860 2861 override @property string stringValue() { return sv; } 2862 override @property int intValue() { return iv; } 2863 } 2864 2865 version(win32_widgets) 2866 override void handleWmCommand(ushort cmd, ushort id) { 2867 if(cmd == CBN_SELCHANGE) { 2868 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2869 fireChangeEvent(); 2870 } 2871 } 2872 2873 private void fireChangeEvent() { 2874 if(selection_ >= options.length) 2875 selection_ = -1; 2876 2877 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2878 t.dispatch(); 2879 } 2880 2881 override int minWidth() { return scaleWithDpi(32); } 2882 2883 version(win32_widgets) { 2884 override int minHeight() { return defaultLineHeight + 6; } 2885 override int maxHeight() { return defaultLineHeight + 6; } 2886 } else { 2887 override int minHeight() { return defaultLineHeight + 4; } 2888 override int maxHeight() { return defaultLineHeight + 4; } 2889 } 2890 2891 version(custom_widgets) 2892 void popup() { 2893 CustomComboBoxPopup popup = new CustomComboBoxPopup(this); 2894 } 2895 2896 } 2897 2898 private class CustomComboBoxPopup : Window { 2899 private ComboboxBase associatedWidget; 2900 private ListWidget lw; 2901 private bool cancelled; 2902 2903 this(ComboboxBase associatedWidget) { 2904 this.associatedWidget = associatedWidget; 2905 2906 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2907 2908 auto w = associatedWidget.width; 2909 // FIXME: suggestedDropdownHeight see below 2910 auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8); 2911 2912 // FIXME: this sux 2913 if(h > associatedWidget.parentWindow.height) 2914 h = associatedWidget.parentWindow.height; 2915 2916 auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok 2917 if(h < mh) 2918 h = mh; 2919 2920 auto coord = associatedWidget.globalCoordinates(); 2921 auto dropDown = new SimpleWindow( 2922 w, h, 2923 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null); 2924 2925 super(dropDown); 2926 2927 dropDown.move(coord.x, coord.y + associatedWidget.height); 2928 2929 this.lw = new ListWidget(this); 2930 version(custom_widgets) 2931 lw.multiSelect = false; 2932 foreach(option; associatedWidget.options) 2933 lw.addOption(option); 2934 2935 auto originalSelection = associatedWidget.getSelection; 2936 lw.setSelection(originalSelection); 2937 lw.scrollSelectionIntoView(); 2938 2939 /+ 2940 { 2941 auto cs = getComputedStyle(); 2942 auto painter = dropDown.draw(); 2943 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2944 auto p = Point(4, 4); 2945 painter.outlineColor = cs.foregroundColor; 2946 foreach(option; associatedWidget.options) { 2947 painter.drawText(p, option); 2948 p.y += defaultLineHeight; 2949 } 2950 } 2951 2952 dropDown.setEventHandlers( 2953 (MouseEvent event) { 2954 if(event.type == MouseEventType.buttonReleased) { 2955 dropDown.close(); 2956 auto element = (event.y - 4) / defaultLineHeight; 2957 if(element >= 0 && element <= associatedWidget.options.length) { 2958 associatedWidget.selection_ = element; 2959 2960 associatedWidget.fireChangeEvent(); 2961 } 2962 } 2963 } 2964 ); 2965 +/ 2966 2967 Widget previouslyFocusedWidget; 2968 2969 dropDown.visibilityChanged = (bool visible) { 2970 if(visible) { 2971 this.redraw(); 2972 captureMouse(this); 2973 2974 if(previouslyFocusedWidget is null) 2975 previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget; 2976 associatedWidget.parentWindow.focusedWidget = lw; 2977 } else { 2978 //dropDown.releaseInputGrab(); 2979 releaseMouseCapture(); 2980 2981 if(!cancelled) 2982 associatedWidget.setSelection(lw.getSelection); 2983 2984 associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget; 2985 } 2986 }; 2987 2988 dropDown.show(); 2989 } 2990 2991 private bool shouldCloseIfClicked(Widget w) { 2992 if(w is this) 2993 return true; 2994 version(custom_widgets) 2995 if(cast(TextListViewWidget.TextListViewItem) w) 2996 return true; 2997 return false; 2998 } 2999 3000 override void defaultEventHandler_click(ClickEvent ce) { 3001 if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) { 3002 this.win.close(); 3003 } 3004 } 3005 3006 override void defaultEventHandler_char(CharEvent ce) { 3007 if(ce.character == '\n') 3008 this.win.close(); 3009 } 3010 3011 override void defaultEventHandler_keydown(KeyDownEvent kde) { 3012 if(kde.key == Key.Escape) { 3013 cancelled = true; 3014 this.win.close(); 3015 }/+ else if(kde.key == Key.Up || kde.key == Key.Down) 3016 {} // intentionally blank, the list view handles these 3017 // separately from the scroll message widget default handler 3018 else if(lw && lw.glvw && lw.glvw.smw) 3019 lw.glvw.smw.defaultKeyboardListener(kde);+/ 3020 } 3021 } 3022 3023 /++ 3024 A drop-down list where the user must select one of the 3025 given options. Like `<select>` in HTML. 3026 3027 The current selection is given as a string or an index. 3028 It emits a SelectionChangedEvent when it changes. 3029 +/ 3030 class DropDownSelection : ComboboxBase { 3031 /++ 3032 Creates a drop down selection, optionally passing its initial list of options. 3033 3034 History: 3035 The overload with the `options` parameter was added December 29, 2024. 3036 +/ 3037 this(Widget parent) { 3038 version(win32_widgets) 3039 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 3040 else version(custom_widgets) { 3041 super(parent); 3042 3043 addEventListener("focus", () { this.redraw; }); 3044 addEventListener("blur", () { this.redraw; }); 3045 addEventListener(EventType.change, () { this.redraw; }); 3046 addEventListener("mousedown", () { this.focus(); this.popup(); }); 3047 addEventListener((KeyDownEvent event) { 3048 if(event.key == Key.Space) 3049 popup(); 3050 }); 3051 } else static assert(false); 3052 } 3053 3054 /// ditto 3055 this(string[] options, Widget parent) { 3056 this(parent); 3057 this.options = options; 3058 } 3059 3060 mixin Padding!q{2}; 3061 static class Style : Widget.Style { 3062 override FrameStyle borderStyle() { return FrameStyle.risen; } 3063 } 3064 mixin OverrideStyle!Style; 3065 3066 version(custom_widgets) 3067 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 3068 auto cs = getComputedStyle(); 3069 3070 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 3071 3072 painter.outlineColor = cs.foregroundColor; 3073 painter.fillColor = cs.foregroundColor; 3074 3075 /+ 3076 Point[4] triangle; 3077 enum padding = 6; 3078 enum paddingV = 7; 3079 enum triangleWidth = 10; 3080 triangle[0] = Point(width - padding - triangleWidth, paddingV); 3081 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 3082 triangle[2] = Point(width - padding - 0, paddingV); 3083 triangle[3] = triangle[0]; 3084 painter.drawPolygon(triangle[]); 3085 +/ 3086 3087 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 3088 3089 painter.drawPolygon( 3090 scaleWithDpi(Point(2, 6) + offset), 3091 scaleWithDpi(Point(7, 11) + offset), 3092 scaleWithDpi(Point(12, 6) + offset), 3093 scaleWithDpi(Point(2, 6) + offset) 3094 ); 3095 3096 3097 return bounds; 3098 } 3099 3100 version(win32_widgets) 3101 override void registerMovement() { 3102 version(win32_widgets) { 3103 if(hwnd) { 3104 auto pos = getChildPositionRelativeToParentHwnd(this); 3105 // the height given to this from Windows' perspective is supposed 3106 // to include the drop down's height. so I add to it to give some 3107 // room for that. 3108 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 3109 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 3110 } 3111 } 3112 sendResizeEvent(); 3113 } 3114 } 3115 3116 /++ 3117 A text box with a drop down arrow listing selections. 3118 The user can choose from the list, or type their own. 3119 +/ 3120 class FreeEntrySelection : ComboboxBase { 3121 this(Widget parent) { 3122 this(null, parent); 3123 } 3124 3125 this(string[] options, Widget parent) { 3126 version(win32_widgets) 3127 super(2 /* CBS_DROPDOWN */, parent); 3128 else version(custom_widgets) { 3129 super(parent); 3130 auto hl = new HorizontalLayout(this); 3131 lineEdit = new LineEdit(hl); 3132 3133 tabStop = false; 3134 3135 // lineEdit.addEventListener((FocusEvent fe) { lineEdit.selectAll(); } ); 3136 3137 auto btn = new class ArrowButton { 3138 this() { 3139 super(ArrowDirection.down, hl); 3140 } 3141 override int heightStretchiness() { 3142 return 1; 3143 } 3144 override int heightShrinkiness() { 3145 return 1; 3146 } 3147 override int maxHeight() { 3148 return lineEdit.maxHeight; 3149 } 3150 }; 3151 //btn.addDirectEventListener("focus", &lineEdit.focus); 3152 btn.addEventListener("triggered", &this.popup); 3153 addEventListener(EventType.change, (Event event) { 3154 lineEdit.content = event.stringValue; 3155 lineEdit.focus(); 3156 redraw(); 3157 }); 3158 } 3159 else static assert(false); 3160 3161 this.options = options; 3162 } 3163 3164 string content() { 3165 version(win32_widgets) 3166 assert(0, "not implemented"); 3167 else version(custom_widgets) 3168 return lineEdit.content; 3169 else static assert(0); 3170 } 3171 3172 void content(string s) { 3173 version(win32_widgets) 3174 assert(0, "not implemented"); 3175 else version(custom_widgets) 3176 lineEdit.content = s; 3177 else static assert(0); 3178 } 3179 3180 version(custom_widgets) { 3181 LineEdit lineEdit; 3182 3183 override int widthStretchiness() { 3184 return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness; 3185 } 3186 override int flexBasisWidth() { 3187 return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth; 3188 } 3189 } 3190 } 3191 3192 /++ 3193 A combination of free entry with a list below it. 3194 +/ 3195 class ComboBox : ComboboxBase { 3196 this(Widget parent) { 3197 version(win32_widgets) 3198 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 3199 else version(custom_widgets) { 3200 super(parent); 3201 lineEdit = new LineEdit(this); 3202 listWidget = new ListWidget(this); 3203 listWidget.multiSelect = false; 3204 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 3205 string c = null; 3206 foreach(option; listWidget.options) 3207 if(option.selected) { 3208 c = option.label; 3209 break; 3210 } 3211 lineEdit.content = c; 3212 }); 3213 3214 listWidget.tabStop = false; 3215 this.tabStop = false; 3216 listWidget.addEventListener("focusin", &lineEdit.focus); 3217 this.addEventListener("focusin", &lineEdit.focus); 3218 3219 addDirectEventListener(EventType.change, { 3220 listWidget.setSelection(selection_); 3221 if(selection_ != -1) 3222 lineEdit.content = options[selection_]; 3223 lineEdit.focus(); 3224 redraw(); 3225 }); 3226 3227 lineEdit.addEventListener("focusin", &lineEdit.selectAll); 3228 3229 listWidget.addDirectEventListener(EventType.change, { 3230 int set = -1; 3231 foreach(idx, opt; listWidget.options) 3232 if(opt.selected) { 3233 set = cast(int) idx; 3234 break; 3235 } 3236 if(set != selection_) 3237 this.setSelection(set); 3238 }); 3239 } else static assert(false); 3240 } 3241 3242 override int minHeight() { return defaultLineHeight * 3; } 3243 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 3244 override int heightStretchiness() { return 5; } 3245 3246 version(custom_widgets) { 3247 LineEdit lineEdit; 3248 ListWidget listWidget; 3249 3250 override void addOption(string s) { 3251 listWidget.addOption(s); 3252 ComboboxBase.addOption(s); 3253 } 3254 3255 override void scrollSelectionIntoView() { 3256 listWidget.scrollSelectionIntoView(); 3257 } 3258 } 3259 } 3260 3261 /+ 3262 class Spinner : Widget { 3263 version(win32_widgets) 3264 this(Widget parent) { 3265 super(parent); 3266 parentWindow = parent.parentWindow; 3267 auto hlayout = new HorizontalLayout(this); 3268 lineEdit = new LineEdit(hlayout); 3269 upDownControl = new UpDownControl(hlayout); 3270 } 3271 3272 LineEdit lineEdit; 3273 UpDownControl upDownControl; 3274 } 3275 3276 class UpDownControl : Widget { 3277 version(win32_widgets) 3278 this(Widget parent) { 3279 super(parent); 3280 parentWindow = parent.parentWindow; 3281 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 3282 } 3283 3284 override int minHeight() { return defaultLineHeight; } 3285 override int maxHeight() { return defaultLineHeight * 3/2; } 3286 3287 override int minWidth() { return defaultLineHeight * 3/2; } 3288 override int maxWidth() { return defaultLineHeight * 3/2; } 3289 } 3290 +/ 3291 3292 /+ 3293 class DataView : Widget { 3294 // this is the omnibus data viewer 3295 // the internal data layout is something like: 3296 // string[string][] but also each node can have parents 3297 } 3298 +/ 3299 3300 3301 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 3302 3303 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 3304 3305 // FIXME: menus should prolly capture the mouse. ugh i kno. 3306 /* 3307 TextEdit needs: 3308 3309 * caret manipulation 3310 * selection control 3311 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 3312 3313 For example: 3314 3315 connect(paste, &textEdit.insertTextAtCaret); 3316 3317 would be nice. 3318 3319 3320 3321 I kinda want an omnibus dataview that combines list, tree, 3322 and table - it can be switched dynamically between them. 3323 3324 Flattening policy: only show top level, show recursive, show grouped 3325 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 3326 3327 Single select, multi select, organization, drag+drop 3328 */ 3329 3330 //static if(UsingSimpledisplayX11) 3331 version(win32_widgets) {} 3332 else version(custom_widgets) { 3333 enum scrollClickRepeatInterval = 50; 3334 3335 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 3336 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 3337 enum activeTabColor = lightAccentColor; 3338 enum hoveringColor = Color(228, 228, 228); 3339 enum buttonColor = windowBackgroundColor; 3340 enum depressedButtonColor = darkAccentColor; 3341 enum activeListXorColor = Color(255, 255, 127); 3342 enum progressBarColor = Color(0, 0, 128); 3343 enum activeMenuItemColor = Color(0, 0, 128); 3344 3345 }} 3346 else static assert(false); 3347 deprecated("Get these properties off the `visualTheme` instead.") { 3348 // these are used by horizontal rule so not just custom_widgets. for now at least. 3349 enum darkAccentColor = Color(172, 172, 172); 3350 enum lightAccentColor = Color(223, 223, 223); // used to be 223 3351 } 3352 3353 private const(wchar)* toWstringzInternal(in char[] s) { 3354 wchar[] str; 3355 str.reserve(s.length + 1); 3356 foreach(dchar ch; s) 3357 str ~= ch; 3358 str ~= '\0'; 3359 return str.ptr; 3360 } 3361 3362 static if(SimpledisplayTimerAvailable) 3363 void setClickRepeat(Widget w, int interval, int delay = 250) { 3364 Timer timer; 3365 int delayRemaining = delay / interval; 3366 if(delayRemaining <= 1) 3367 delayRemaining = 2; 3368 3369 immutable originalDelayRemaining = delayRemaining; 3370 3371 w.addDirectEventListener((scope MouseDownEvent ev) { 3372 if(ev.srcElement !is w) 3373 return; 3374 if(timer !is null) { 3375 timer.destroy(); 3376 timer = null; 3377 } 3378 delayRemaining = originalDelayRemaining; 3379 timer = new Timer(interval, () { 3380 if(delayRemaining > 0) 3381 delayRemaining--; 3382 else { 3383 auto ev = new Event("triggered", w); 3384 ev.sendDirectly(); 3385 } 3386 }); 3387 }); 3388 3389 w.addDirectEventListener((scope MouseUpEvent ev) { 3390 if(ev.srcElement !is w) 3391 return; 3392 if(timer !is null) { 3393 timer.destroy(); 3394 timer = null; 3395 } 3396 }); 3397 3398 w.addDirectEventListener((scope MouseLeaveEvent ev) { 3399 if(ev.srcElement !is w) 3400 return; 3401 if(timer !is null) { 3402 timer.destroy(); 3403 timer = null; 3404 } 3405 }); 3406 3407 } 3408 else 3409 void setClickRepeat(Widget w, int interval, int delay = 250) {} 3410 3411 enum FrameStyle { 3412 none, /// 3413 risen, /// a 3d pop-out effect (think Windows 95 button) 3414 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 3415 solid, /// 3416 dotted, /// 3417 fantasy, /// a style based on a popular fantasy video game 3418 rounded, /// a rounded rectangle 3419 } 3420 3421 version(custom_widgets) 3422 deprecated 3423 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 3424 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3425 } 3426 3427 version(custom_widgets) 3428 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 3429 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 3430 } 3431 3432 version(custom_widgets) 3433 deprecated 3434 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 3435 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3436 } 3437 3438 int getBorderWidth(FrameStyle style) { 3439 final switch(style) { 3440 case FrameStyle.sunk, FrameStyle.risen: 3441 return 2; 3442 case FrameStyle.none: 3443 return 0; 3444 case FrameStyle.solid: 3445 return 1; 3446 case FrameStyle.dotted: 3447 return 1; 3448 case FrameStyle.fantasy: 3449 return 3; 3450 case FrameStyle.rounded: 3451 return 2; 3452 } 3453 } 3454 3455 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 3456 int borderWidth = getBorderWidth(style); 3457 final switch(style) { 3458 case FrameStyle.sunk, FrameStyle.risen: 3459 // outer layer 3460 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 3461 break; 3462 case FrameStyle.none: 3463 painter.outlineColor = background; 3464 break; 3465 case FrameStyle.solid: 3466 case FrameStyle.rounded: 3467 painter.pen = Pen(border, 1); 3468 break; 3469 case FrameStyle.dotted: 3470 painter.pen = Pen(border, 1, Pen.Style.Dotted); 3471 break; 3472 case FrameStyle.fantasy: 3473 painter.pen = Pen(border, 3); 3474 break; 3475 } 3476 3477 painter.fillColor = background; 3478 3479 if(style == FrameStyle.rounded) { 3480 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 3481 } else { 3482 painter.drawRectangle(Point(x + 0, y + 0), width, height); 3483 3484 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 3485 // 3d effect 3486 auto vt = WidgetPainter.visualTheme; 3487 3488 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 3489 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 3490 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 3491 3492 // inner layer 3493 //right, bottom 3494 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 3495 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 3496 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 3497 // left, top 3498 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 3499 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 3500 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 3501 } else if(style == FrameStyle.fantasy) { 3502 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 3503 painter.fillColor = Color.transparent; 3504 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 3505 } 3506 } 3507 3508 return borderWidth; 3509 } 3510 3511 /++ 3512 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. 3513 3514 See_Also: 3515 [MenuItem] 3516 [ToolButton] 3517 [Menu.addItem] 3518 +/ 3519 class Action { 3520 version(win32_widgets) { 3521 private int id; 3522 private static int lastId = 9000; 3523 private static Action[int] mapping; 3524 } 3525 3526 KeyEvent accelerator; 3527 3528 // FIXME: disable message 3529 // and toggle thing? 3530 // ??? and trigger arguments too ??? 3531 3532 /++ 3533 Params: 3534 label = the textual label 3535 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 3536 triggered = initial handler, more can be added via the [triggered] member. 3537 +/ 3538 this(string label, ushort icon = 0, void delegate() triggered = null) { 3539 this.label = label; 3540 this.iconId = icon; 3541 if(triggered !is null) 3542 this.triggered ~= triggered; 3543 version(win32_widgets) { 3544 id = ++lastId; 3545 mapping[id] = this; 3546 } 3547 } 3548 3549 private string label; 3550 private ushort iconId; 3551 // icon 3552 3553 // when it is triggered, the triggered event is fired on the window 3554 /// The list of handlers when it is triggered. 3555 void delegate()[] triggered; 3556 } 3557 3558 /* 3559 plan: 3560 keyboard accelerators 3561 3562 * menus (and popups and tooltips) 3563 * status bar 3564 * toolbars and buttons 3565 3566 sortable table view 3567 3568 maybe notification area icons 3569 basic clipboard 3570 3571 * radio box 3572 splitter 3573 toggle buttons (optionally mutually exclusive, like in Paint) 3574 label, rich text display, multi line plain text (selectable) 3575 * fieldset 3576 * nestable grid layout 3577 single line text input 3578 * multi line text input 3579 slider 3580 spinner 3581 list box 3582 drop down 3583 combo box 3584 auto complete box 3585 * progress bar 3586 3587 terminal window/widget (on unix it might even be a pty but really idk) 3588 3589 ok button 3590 cancel button 3591 3592 keyboard hotkeys 3593 3594 scroll widget 3595 3596 event redirections and network transparency 3597 script integration 3598 */ 3599 3600 3601 /* 3602 MENUS 3603 3604 auto bar = new MenuBar(window); 3605 window.menuBar = bar; 3606 3607 auto fileMenu = bar.addItem(new Menu("&File")); 3608 fileMenu.addItem(new MenuItem("&Exit")); 3609 3610 3611 EVENTS 3612 3613 For controls, you should usually use "triggered" rather than "click", etc., because 3614 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3615 This is the case on menus and pushbuttons. 3616 3617 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3618 */ 3619 3620 3621 /* 3622 enum LinePreference { 3623 AlwaysOnOwnLine, // always on its own line 3624 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3625 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 3626 } 3627 */ 3628 3629 /++ 3630 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. 3631 3632 --- 3633 class MyWidget : Widget { 3634 this(Widget parent) { super(parent); } 3635 3636 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3637 mixin Padding!q{4}; 3638 3639 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3640 mixin Margin!q{8}; 3641 3642 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3643 // while Top/Bottom/Right remain 8 from the mixin above. 3644 override int marginLeft() { return 2; } 3645 } 3646 --- 3647 3648 3649 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]). 3650 3651 Padding is the area inside a widget where its background is drawn, but the content avoids. 3652 3653 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!). 3654 3655 * 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. 3656 +/ 3657 mixin template Padding(string code) { 3658 override int paddingLeft() { return mixin(code);} 3659 override int paddingRight() { return mixin(code);} 3660 override int paddingTop() { return mixin(code);} 3661 override int paddingBottom() { return mixin(code);} 3662 } 3663 3664 /// ditto 3665 mixin template Margin(string code) { 3666 override int marginLeft() { return mixin(code);} 3667 override int marginRight() { return mixin(code);} 3668 override int marginTop() { return mixin(code);} 3669 override int marginBottom() { return mixin(code);} 3670 } 3671 3672 private 3673 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3674 enum calcingV = relevantMeasure == "height"; 3675 3676 parent.registerMovement(); 3677 3678 if(parent.children.length == 0) 3679 return; 3680 3681 auto parentStyle = parent.getComputedStyle(); 3682 3683 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3684 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3685 3686 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3687 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3688 3689 // my own width and height should already be set by the caller of this function... 3690 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3691 mixin("parentStyle.padding"~firstThingy~"()") - 3692 mixin("parentStyle.padding"~secondThingy~"()"); 3693 3694 int stretchinessSum; 3695 int stretchyChildSum; 3696 int lastMargin = 0; 3697 3698 int shrinkinessSum; 3699 int shrinkyChildSum; 3700 3701 // set initial size 3702 foreach(child; parent.children) { 3703 3704 auto childStyle = child.getComputedStyle(); 3705 3706 if(cast(StaticPosition) child) 3707 continue; 3708 if(child.hidden) 3709 continue; 3710 3711 const iw = child.flexBasisWidth(); 3712 const ih = child.flexBasisHeight(); 3713 3714 static if(calcingV) { 3715 child.width = parent.width - 3716 mixin("childStyle.margin"~otherFirstThingy~"()") - 3717 mixin("childStyle.margin"~otherSecondThingy~"()") - 3718 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3719 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3720 3721 if(child.width < 0) 3722 child.width = 0; 3723 if(child.width > childStyle.maxWidth()) 3724 child.width = childStyle.maxWidth(); 3725 3726 if(iw > 0) { 3727 auto totalPossible = child.width; 3728 if(child.width > iw && child.widthStretchiness() == 0) 3729 child.width = iw; 3730 } 3731 3732 child.height = mymax(childStyle.minHeight(), ih); 3733 } else { 3734 // set to take all the space 3735 child.height = parent.height - 3736 mixin("childStyle.margin"~firstThingy~"()") - 3737 mixin("childStyle.margin"~secondThingy~"()") - 3738 mixin("parentStyle.padding"~firstThingy~"()") - 3739 mixin("parentStyle.padding"~secondThingy~"()"); 3740 3741 // then clamp it 3742 if(child.height < 0) 3743 child.height = 0; 3744 if(child.height > childStyle.maxHeight()) 3745 child.height = childStyle.maxHeight(); 3746 3747 // and if possible, respect the ideal target 3748 if(ih > 0) { 3749 auto totalPossible = child.height; 3750 if(child.height > ih && child.heightStretchiness() == 0) 3751 child.height = ih; 3752 } 3753 3754 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3755 child.width = mymax(childStyle.minWidth(), iw); 3756 } 3757 3758 spaceRemaining -= mixin("child." ~ relevantMeasure); 3759 3760 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3761 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3762 lastMargin = margin; 3763 spaceRemaining -= thisMargin + margin; 3764 3765 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3766 stretchinessSum += s; 3767 if(s > 0) 3768 stretchyChildSum++; 3769 3770 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3771 shrinkinessSum += s2; 3772 if(s2 > 0) 3773 shrinkyChildSum++; 3774 } 3775 3776 if(spaceRemaining < 0 && shrinkyChildSum) { 3777 // shrink to get into the space if it is possible 3778 auto toRemove = -spaceRemaining; 3779 auto removalPerItem = toRemove / shrinkinessSum; 3780 auto remainder = toRemove % shrinkinessSum; 3781 3782 // FIXME: wtf why am i shrinking things with no shrinkiness? 3783 3784 foreach(child; parent.children) { 3785 auto childStyle = child.getComputedStyle(); 3786 if(cast(StaticPosition) child) 3787 continue; 3788 if(child.hidden) 3789 continue; 3790 static if(calcingV) { 3791 auto minimum = childStyle.minHeight(); 3792 auto stretch = childStyle.heightShrinkiness(); 3793 } else { 3794 auto minimum = childStyle.minWidth(); 3795 auto stretch = childStyle.widthShrinkiness(); 3796 } 3797 3798 if(mixin("child._" ~ relevantMeasure) <= minimum) 3799 continue; 3800 // import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch); 3801 3802 mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh. 3803 3804 spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum; 3805 } 3806 } 3807 3808 // stretch to fill space 3809 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3810 auto spacePerChild = spaceRemaining / stretchinessSum; 3811 bool spreadEvenly; 3812 bool giveToBiggest; 3813 if(spacePerChild <= 0) { 3814 spacePerChild = spaceRemaining / stretchyChildSum; 3815 spreadEvenly = true; 3816 } 3817 if(spacePerChild <= 0) { 3818 giveToBiggest = true; 3819 } 3820 int previousSpaceRemaining = spaceRemaining; 3821 stretchinessSum = 0; 3822 Widget mostStretchy; 3823 int mostStretchyS; 3824 foreach(child; parent.children) { 3825 auto childStyle = child.getComputedStyle(); 3826 if(cast(StaticPosition) child) 3827 continue; 3828 if(child.hidden) 3829 continue; 3830 static if(calcingV) { 3831 auto maximum = childStyle.maxHeight(); 3832 } else { 3833 auto maximum = childStyle.maxWidth(); 3834 } 3835 3836 if(mixin("child." ~ relevantMeasure) >= maximum) { 3837 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3838 mixin("child._" ~ relevantMeasure) -= adj; 3839 spaceRemaining += adj; 3840 continue; 3841 } 3842 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3843 if(s <= 0) 3844 continue; 3845 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3846 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3847 spaceRemaining -= spaceAdjustment; 3848 if(mixin("child." ~ relevantMeasure) > maximum) { 3849 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3850 mixin("child._" ~ relevantMeasure) -= diff; 3851 spaceRemaining += diff; 3852 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3853 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3854 if(mostStretchy is null || s >= mostStretchyS) { 3855 mostStretchy = child; 3856 mostStretchyS = s; 3857 } 3858 } 3859 } 3860 3861 if(giveToBiggest && mostStretchy !is null) { 3862 auto child = mostStretchy; 3863 auto childStyle = child.getComputedStyle(); 3864 int spaceAdjustment = spaceRemaining; 3865 3866 static if(calcingV) 3867 auto maximum = childStyle.maxHeight(); 3868 else 3869 auto maximum = childStyle.maxWidth(); 3870 3871 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3872 spaceRemaining -= spaceAdjustment; 3873 if(mixin("child._" ~ relevantMeasure) > maximum) { 3874 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3875 mixin("child._" ~ relevantMeasure) -= diff; 3876 spaceRemaining += diff; 3877 } 3878 } 3879 3880 if(spaceRemaining == previousSpaceRemaining) { 3881 if(mostStretchy !is null) { 3882 static if(calcingV) 3883 auto maximum = mostStretchy.maxHeight(); 3884 else 3885 auto maximum = mostStretchy.maxWidth(); 3886 3887 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3888 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3889 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3890 } 3891 break; // apparently nothing more we can do 3892 } 3893 } 3894 3895 foreach(child; parent.children) { 3896 auto childStyle = child.getComputedStyle(); 3897 if(cast(StaticPosition) child) 3898 continue; 3899 if(child.hidden) 3900 continue; 3901 3902 static if(calcingV) 3903 auto maximum = childStyle.maxHeight(); 3904 else 3905 auto maximum = childStyle.maxWidth(); 3906 if(mixin("child._" ~ relevantMeasure) > maximum) 3907 mixin("child._" ~ relevantMeasure) = maximum; 3908 } 3909 3910 // position 3911 lastMargin = 0; 3912 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3913 foreach(child; parent.children) { 3914 auto childStyle = child.getComputedStyle(); 3915 if(cast(StaticPosition) child) { 3916 child.recomputeChildLayout(); 3917 continue; 3918 } 3919 if(child.hidden) 3920 continue; 3921 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3922 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3923 currentPos += thisMargin; 3924 static if(calcingV) { 3925 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3926 child.y = currentPos; 3927 } else { 3928 child.x = currentPos; 3929 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3930 3931 } 3932 currentPos += mixin("child." ~ relevantMeasure); 3933 currentPos += margin; 3934 lastMargin = margin; 3935 3936 child.recomputeChildLayout(); 3937 } 3938 } 3939 3940 int mymax(int a, int b) { return a > b ? a : b; } 3941 int mymax(int a, int b, int c) { 3942 auto d = mymax(a, b); 3943 return c > d ? c : d; 3944 } 3945 3946 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3947 // and here, it must be integrable with the layout, the event system, and not be painted over. 3948 version(win32_widgets) { 3949 3950 // this function just does stuff that a parent window needs for redirection 3951 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3952 this_.hookedWndProc(msg, wParam, lParam); 3953 3954 switch(msg) { 3955 3956 case WM_VSCROLL, WM_HSCROLL: 3957 auto pos = HIWORD(wParam); 3958 auto m = LOWORD(wParam); 3959 3960 auto scrollbarHwnd = cast(HWND) lParam; 3961 3962 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3963 3964 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3965 3966 switch(m) { 3967 /+ 3968 // I don't think those messages are ever actually sent normally by the widget itself, 3969 // they are more used for the keyboard interface. methinks. 3970 case SB_BOTTOM: 3971 // writeln("end"); 3972 auto event = new Event("scrolltoend", *widgetp); 3973 event.dispatch(); 3974 //if(!event.defaultPrevented) 3975 break; 3976 case SB_TOP: 3977 // writeln("top"); 3978 auto event = new Event("scrolltobeginning", *widgetp); 3979 event.dispatch(); 3980 break; 3981 case SB_ENDSCROLL: 3982 // idk 3983 break; 3984 +/ 3985 case SB_LINEDOWN: 3986 (*widgetp).emitCommand!"scrolltonextline"(); 3987 return 0; 3988 case SB_LINEUP: 3989 (*widgetp).emitCommand!"scrolltopreviousline"(); 3990 return 0; 3991 case SB_PAGEDOWN: 3992 (*widgetp).emitCommand!"scrolltonextpage"(); 3993 return 0; 3994 case SB_PAGEUP: 3995 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3996 return 0; 3997 case SB_THUMBPOSITION: 3998 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3999 ev.dispatch(); 4000 return 0; 4001 case SB_THUMBTRACK: 4002 // eh kinda lying but i like the real time update display 4003 auto ev = new ScrollToPositionEvent(*widgetp, pos); 4004 ev.dispatch(); 4005 4006 // the event loop doesn't seem to carry on with a requested redraw.. 4007 // so we request it to get our dirty bit set... 4008 // then we need to immediately actually redraw it too for instant feedback to user 4009 SimpleWindow.processAllCustomEvents(); 4010 SimpleWindow.processAllCustomEvents(); 4011 //if(this_.parentWindow) 4012 //this_.parentWindow.actualRedraw(); 4013 4014 // and this ensures the WM_PAINT message is sent fairly quickly 4015 // still seems to lag a little in large windows but meh it basically works. 4016 if(this_.parentWindow) { 4017 // FIXME: if painting is slow, this does still lag 4018 // we probably will want to expose some user hook to ScrollWindowEx 4019 // or something. 4020 UpdateWindow(this_.parentWindow.hwnd); 4021 } 4022 return 0; 4023 default: 4024 } 4025 } 4026 break; 4027 4028 case WM_CONTEXTMENU: 4029 auto hwndFrom = cast(HWND) wParam; 4030 4031 auto xPos = cast(short) LOWORD(lParam); 4032 auto yPos = cast(short) HIWORD(lParam); 4033 4034 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4035 POINT p; 4036 p.x = xPos; 4037 p.y = yPos; 4038 ScreenToClient(hwnd, &p); 4039 auto clientX = cast(ushort) p.x; 4040 auto clientY = cast(ushort) p.y; 4041 4042 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 4043 4044 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 4045 return 0; 4046 } 4047 } 4048 break; 4049 4050 case WM_DRAWITEM: 4051 auto dis = cast(DRAWITEMSTRUCT*) lParam; 4052 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 4053 return (*widgetp).handleWmDrawItem(dis); 4054 } 4055 break; 4056 4057 case WM_NOTIFY: 4058 auto hdr = cast(NMHDR*) lParam; 4059 auto hwndFrom = hdr.hwndFrom; 4060 auto code = hdr.code; 4061 4062 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4063 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 4064 } 4065 break; 4066 case WM_COMMAND: 4067 auto handle = cast(HWND) lParam; 4068 auto cmd = HIWORD(wParam); 4069 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 4070 4071 default: 4072 // pass it on 4073 } 4074 return 0; 4075 } 4076 4077 4078 4079 extern(Windows) 4080 private 4081 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 4082 // but can i merge them?! 4083 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4084 // try { writeln(iMessage); } catch(Exception e) {}; 4085 4086 if(auto te = hWnd in Widget.nativeMapping) { 4087 try { 4088 4089 te.hookedWndProc(iMessage, wParam, lParam); 4090 4091 int mustReturn; 4092 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 4093 if(mustReturn) 4094 return ret; 4095 4096 if(iMessage == WM_SETFOCUS) { 4097 auto lol = *te; 4098 while(lol !is null && lol.implicitlyCreated) 4099 lol = lol.parent; 4100 lol.focus(); 4101 //(*te).parentWindow.focusedWidget = lol; 4102 } 4103 4104 4105 if(iMessage == WM_CTLCOLOREDIT) { 4106 4107 } 4108 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 4109 SetBkMode(cast(HDC) wParam, TRANSPARENT); 4110 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 4111 //GetStockObject(NULL_BRUSH); 4112 } 4113 4114 auto pos = getChildPositionRelativeToParentOrigin(*te); 4115 lastDefaultPrevented = false; 4116 // try { writeln(typeid(*te)); } catch(Exception e) {} 4117 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 4118 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 4119 else { 4120 // it was something we recognized, should only call the window procedure if the default was not prevented 4121 } 4122 } catch(Exception e) { 4123 assert(0, e.toString()); 4124 } 4125 return 0; 4126 } 4127 assert(0, "shouldn't be receiving messages for this window...."); 4128 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 4129 } 4130 4131 extern(Windows) 4132 private 4133 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 4134 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4135 if(iMessage == WM_ERASEBKGND) { 4136 auto dc = GetDC(hWnd); 4137 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 4138 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 4139 RECT r; 4140 GetWindowRect(hWnd, &r); 4141 // since the pen is null, to fill the whole space, we need the +1 on both. 4142 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 4143 SelectObject(dc, p); 4144 SelectObject(dc, b); 4145 ReleaseDC(hWnd, dc); 4146 InvalidateRect(hWnd, null, false); // redraw the border 4147 return 1; 4148 } 4149 return HookedWndProc(hWnd, iMessage, wParam, lParam); 4150 } 4151 4152 /++ 4153 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 4154 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 4155 of minigui's expectations. 4156 4157 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 4158 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 4159 4160 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 4161 4162 To check if you can use this, use `static if(UsingWin32Widgets)`. 4163 +/ 4164 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 4165 assert(p.parentWindow !is null); 4166 assert(p.parentWindow.win.impl.hwnd !is null); 4167 4168 auto bsgroupbox = style == BS_GROUPBOX; 4169 4170 HWND phwnd; 4171 4172 auto wtf = p.parent; 4173 while(wtf) { 4174 if(wtf.hwnd !is null) { 4175 phwnd = wtf.hwnd; 4176 break; 4177 } 4178 wtf = wtf.parent; 4179 } 4180 4181 if(phwnd is null) 4182 phwnd = p.parentWindow.win.impl.hwnd; 4183 4184 assert(phwnd !is null); 4185 4186 WCharzBuffer wt = WCharzBuffer(windowText); 4187 4188 style |= WS_VISIBLE | WS_CHILD; 4189 //if(className != WC_TABCONTROL) 4190 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 4191 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 4192 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 4193 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 4194 4195 assert(p.hwnd !is null); 4196 4197 4198 static HFONT font; 4199 if(font is null) { 4200 NONCLIENTMETRICS params; 4201 params.cbSize = params.sizeof; 4202 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 4203 font = CreateFontIndirect(¶ms.lfMessageFont); 4204 } 4205 } 4206 4207 if(font) 4208 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 4209 4210 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 4211 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 4212 Widget.nativeMapping[p.hwnd] = p; 4213 4214 if(bsgroupbox) 4215 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 4216 else 4217 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4218 4219 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 4220 4221 p.registerMovement(); 4222 } 4223 } 4224 4225 version(win32_widgets) 4226 private 4227 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 4228 if(hwnd is null || hwnd in Widget.nativeMapping) 4229 return true; 4230 auto parent = cast(Widget) cast(void*) lparam; 4231 Widget p = new Widget(null); 4232 p._parent = parent; 4233 p.parentWindow = parent.parentWindow; 4234 p.hwnd = hwnd; 4235 p.implicitlyCreated = true; 4236 Widget.nativeMapping[p.hwnd] = p; 4237 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4238 return true; 4239 } 4240 4241 /++ 4242 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 4243 +/ 4244 struct WidgetPainter { 4245 this(ScreenPainter screenPainter, Widget drawingUpon) { 4246 this.drawingUpon = drawingUpon; 4247 this.screenPainter = screenPainter; 4248 4249 this.widgetClipRectangle = screenPainter.currentClipRectangle; 4250 4251 // this.screenPainter.impl.enableXftDraw(); 4252 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4253 this.screenPainter.setFont(font); 4254 } 4255 4256 /++ 4257 EXPERIMENTAL. subject to change. 4258 4259 When you draw a cursor, you can draw this to notify your window of where it is, 4260 for IME systems to use. 4261 +/ 4262 void notifyCursorPosition(int x, int y, int width, int height) { 4263 if(auto a = drawingUpon.parentWindow) 4264 if(auto w = a.inputProxy) { 4265 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 4266 } 4267 } 4268 4269 private Rectangle widgetClipRectangle; 4270 4271 private Rectangle setClipRectangleForWidget(Point upperLeft, int width, int height) { 4272 widgetClipRectangle = Rectangle(upperLeft, Size(width, height)); 4273 4274 return screenPainter.setClipRectangle(widgetClipRectangle); 4275 } 4276 4277 /++ 4278 Sets the clip rectangle to the given settings. It will automatically calculate the intersection 4279 of your widget's content boundaries and your requested clip rectangle. 4280 4281 History: 4282 Before February 26, 2025, you could sometimes exceed widget boundaries, as this forwarded 4283 directly to the underlying `ScreenPainter`. It now wraps it to calculate the intersection. 4284 +/ 4285 Rectangle setClipRectangle(Rectangle rectangle) { 4286 return screenPainter.setClipRectangle(rectangle.intersectionOf(widgetClipRectangle)); 4287 } 4288 /// ditto 4289 Rectangle setClipRectangle(Point upperLeft, int width, int height) { 4290 return setClipRectangle(Rectangle(upperLeft, Size(width, height))); 4291 } 4292 /// ditto 4293 Rectangle setClipRectangle(Point upperLeft, Size size) { 4294 return setClipRectangle(Rectangle(upperLeft, size)); 4295 } 4296 4297 /// 4298 ScreenPainter screenPainter; 4299 /// Forward to the screen painter for all other methods, see [arsd.simpledisplay.ScreenPainter] for more information 4300 alias screenPainter this; 4301 4302 private Widget drawingUpon; 4303 4304 /++ 4305 This is the list of rectangles that actually need to be redrawn. 4306 4307 Not actually implemented yet. 4308 +/ 4309 Rectangle[] invalidatedRectangles; 4310 4311 private static BaseVisualTheme _visualTheme; 4312 4313 /++ 4314 Functions to access the visual theme and helpers to easily use it. 4315 4316 These are aware of the current widget's computed style out of the theme. 4317 +/ 4318 static @property BaseVisualTheme visualTheme() { 4319 if(_visualTheme is null) 4320 _visualTheme = new DefaultVisualTheme(); 4321 return _visualTheme; 4322 } 4323 4324 /// ditto 4325 static @property void visualTheme(BaseVisualTheme theme) { 4326 _visualTheme = theme; 4327 4328 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 4329 } 4330 4331 /// ditto 4332 Color themeForeground() { 4333 return drawingUpon.getComputedStyle().foregroundColor(); 4334 } 4335 4336 /// ditto 4337 Color themeBackground() { 4338 return drawingUpon.getComputedStyle().background.color; 4339 } 4340 4341 int isDarkTheme() { 4342 return 0; // unspecified, yes, no as enum. FIXME 4343 } 4344 4345 /++ 4346 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. 4347 4348 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 4349 4350 If you change teh clip rectangle, you should change it back before you return. 4351 4352 4353 The sequence it uses is: 4354 background 4355 content (delegated to you) 4356 border 4357 focused outline 4358 selected overlay 4359 4360 Example code: 4361 4362 --- 4363 void paint(WidgetPainter painter) { 4364 painter.drawThemed((bounds) { 4365 return bounds; // if the selection overlay should be contained, you can return it here. 4366 }); 4367 } 4368 --- 4369 +/ 4370 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 4371 drawThemed((WidgetPainter painter, const Rectangle bounds) { 4372 return drawBody(bounds); 4373 }); 4374 } 4375 // this overload is actually mroe for setting the delegate to a virtual function 4376 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 4377 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 4378 4379 auto cs = drawingUpon.getComputedStyle(); 4380 4381 auto bg = cs.background.color; 4382 4383 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 4384 4385 rect.left += borderWidth; 4386 rect.right -= borderWidth; 4387 rect.top += borderWidth; 4388 rect.bottom -= borderWidth; 4389 4390 auto insideBorderRect = rect; 4391 4392 rect.left += cs.paddingLeft; 4393 rect.right -= cs.paddingRight; 4394 rect.top += cs.paddingTop; 4395 rect.bottom -= cs.paddingBottom; 4396 4397 this.outlineColor = this.themeForeground; 4398 this.fillColor = bg; 4399 4400 auto widgetFont = cs.fontCached; 4401 if(widgetFont !is null) 4402 this.setFont(widgetFont); 4403 4404 rect = drawBody(this, rect); 4405 4406 if(widgetFont !is null) { 4407 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4408 this.setFont(vtFont); 4409 else 4410 this.setFont(null); 4411 } 4412 4413 if(auto os = cs.outlineStyle()) { 4414 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 4415 this.fillColor = Color.transparent; 4416 this.drawRectangle(insideBorderRect); 4417 } 4418 } 4419 4420 /++ 4421 First, draw the background. 4422 Then draw your content. 4423 Next, draw the border. 4424 And the focused indicator. 4425 And the is-selected box. 4426 4427 If it is focused i can draw the outline too... 4428 4429 If selected i can even do the xor action but that's at the end. 4430 +/ 4431 void drawThemeBackground() { 4432 4433 } 4434 4435 void drawThemeBorder() { 4436 4437 } 4438 4439 // all this stuff is a dangerous experiment.... 4440 static class ScriptableVersion { 4441 ScreenPainterImplementation* p; 4442 int originX, originY; 4443 4444 @scriptable: 4445 void drawRectangle(int x, int y, int width, int height) { 4446 p.drawRectangle(x + originX, y + originY, width, height); 4447 } 4448 void drawLine(int x1, int y1, int x2, int y2) { 4449 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 4450 } 4451 void drawText(int x, int y, string text) { 4452 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 4453 } 4454 void setOutlineColor(int r, int g, int b) { 4455 p.pen = Pen(Color(r,g,b), 1); 4456 } 4457 void setFillColor(int r, int g, int b) { 4458 p.fillColor = Color(r,g,b); 4459 } 4460 } 4461 4462 ScriptableVersion toArsdJsvar() { 4463 auto sv = new ScriptableVersion; 4464 sv.p = this.screenPainter.impl; 4465 sv.originX = this.screenPainter.originX; 4466 sv.originY = this.screenPainter.originY; 4467 return sv; 4468 } 4469 4470 static WidgetPainter fromJsVar(T)(T t) { 4471 return WidgetPainter.init; 4472 } 4473 // done.......... 4474 } 4475 4476 4477 struct Style { 4478 static struct helper(string m, T) { 4479 enum method = m; 4480 T v; 4481 4482 mixin template MethodOverride(typeof(this) v) { 4483 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 4484 } 4485 } 4486 4487 static auto opDispatch(string method, T)(T value) { 4488 return helper!(method, T)(value); 4489 } 4490 } 4491 4492 /++ 4493 Implementation detail of the [ControlledBy] UDA. 4494 4495 History: 4496 Added Oct 28, 2020 4497 +/ 4498 struct ControlledBy_(T, Args...) { 4499 Args args; 4500 4501 static if(Args.length) 4502 this(Args args) { 4503 this.args = args; 4504 } 4505 4506 private T construct(Widget parent) { 4507 return new T(args, parent); 4508 } 4509 } 4510 4511 /++ 4512 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 4513 4514 History: 4515 Added Oct 28, 2020 4516 +/ 4517 auto ControlledBy(T, Args...)(Args args) { 4518 return ControlledBy_!(T, Args)(args); 4519 } 4520 4521 struct ContainerMeta { 4522 string name; 4523 ContainerMeta[] children; 4524 Widget function(Widget parent) factory; 4525 4526 Widget instantiate(Widget parent) { 4527 auto n = factory(parent); 4528 n.name = name; 4529 foreach(child; children) 4530 child.instantiate(n); 4531 return n; 4532 } 4533 } 4534 4535 /++ 4536 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 4537 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 4538 4539 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 4540 structures. It works fine on structs declared inside functions though. 4541 4542 See: https://issues.dlang.org/show_bug.cgi?id=21984 4543 +/ 4544 template Container(CArgs...) { 4545 static if(CArgs.length && is(CArgs[0] : Widget)) { 4546 private alias Super = CArgs[0]; 4547 private alias CArgs2 = CArgs[1 .. $]; 4548 } else { 4549 private alias Super = Layout; 4550 private alias CArgs2 = CArgs; 4551 } 4552 4553 class Container : Super { 4554 this(Widget parent) { super(parent); } 4555 4556 // just to partially support old gdc versions 4557 version(GNU) { 4558 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 4559 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 4560 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 4561 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 4562 } else mixin(q{ 4563 static foreach(Arg; CArgs2) { 4564 mixin Arg.MethodOverride!(Arg); 4565 } 4566 }); 4567 4568 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 4569 return ContainerMeta( 4570 name, 4571 children.dup, 4572 function (Widget parent) { return new typeof(this)(parent); } 4573 ); 4574 } 4575 4576 static ContainerMeta opCall(ContainerMeta[] children...) { 4577 return opCall(null, children); 4578 } 4579 } 4580 } 4581 4582 /++ 4583 The data controller widget is created by reflecting over the given 4584 data type. You can use [ControlledBy] as a UDA on a struct or 4585 just let it create things automatically. 4586 4587 Unlike [dialog], this uses real-time updating of the data and 4588 you add it to another window yourself. 4589 4590 --- 4591 struct Test { 4592 int x; 4593 int y; 4594 } 4595 4596 auto window = new Window(); 4597 auto dcw = new DataControllerWidget!Test(new Test, window); 4598 --- 4599 4600 The way it works is any public members are given a widget based 4601 on their data type, and public methods trigger an action button 4602 if no relevant parameters or a dialog action if it does have 4603 parameters, similar to the [menu] facility. 4604 4605 If you change data programmatically, without going through the 4606 DataControllerWidget methods, you will have to tell it something 4607 has changed and it needs to redraw. This is done with the `invalidate` 4608 method. 4609 4610 History: 4611 Added Oct 28, 2020 4612 +/ 4613 /// Group: generating_from_code 4614 class DataControllerWidget(T) : WidgetContainer { 4615 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 4616 private alias Tref = T; 4617 else 4618 private alias Tref = T*; 4619 4620 Tref datum; 4621 4622 /++ 4623 See_also: [addDataControllerWidget] 4624 +/ 4625 this(Tref datum, Widget parent) { 4626 this.datum = datum; 4627 4628 Widget cp = this; 4629 4630 super(parent); 4631 4632 foreach(attr; __traits(getAttributes, T)) 4633 static if(is(typeof(attr) == ContainerMeta)) { 4634 cp = attr.instantiate(this); 4635 } 4636 4637 auto def = this.getByName("default"); 4638 if(def !is null) 4639 cp = def; 4640 4641 Widget helper(string name) { 4642 auto maybe = this.getByName(name); 4643 if(maybe is null) 4644 return cp; 4645 return maybe; 4646 4647 } 4648 4649 foreach(member; __traits(allMembers, T)) 4650 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4651 static if(is(typeof(__traits(getMember, this.datum, member)))) 4652 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4653 void delegate() update; 4654 4655 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4656 4657 if(update) 4658 updaters ~= update; 4659 4660 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4661 w.addEventListener("triggered", delegate() { 4662 makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))(); 4663 notifyDataUpdated(); 4664 }); 4665 } else static if(is(typeof(w.isChecked) == bool)) { 4666 w.addEventListener(EventType.change, (Event ev) { 4667 __traits(getMember, this.datum, member) = w.isChecked; 4668 }); 4669 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4670 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4671 } else static if(is(typeof(w.value) == int)) { 4672 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4673 } else static if(is(typeof(w) == DropDownSelection)) { 4674 // special case for this to kinda support enums and such. coudl be better though 4675 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4676 } else { 4677 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4678 } 4679 } 4680 } 4681 4682 /++ 4683 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4684 4685 History: 4686 Added May 28, 2021 4687 +/ 4688 void notifyDataUpdated() { 4689 foreach(updater; updaters) 4690 updater(); 4691 4692 this.emit!(ChangeEvent!void)(delegate{}); 4693 } 4694 4695 private Widget[string] memberWidgets; 4696 private void delegate()[] updaters; 4697 4698 mixin Emits!(ChangeEvent!void); 4699 } 4700 4701 private int saturatedSum(int[] values...) { 4702 int sum; 4703 foreach(value; values) { 4704 if(value == int.max) 4705 return int.max; 4706 sum += value; 4707 } 4708 return sum; 4709 } 4710 4711 void genericSetValue(T, W)(T* where, W what) { 4712 import std.conv; 4713 *where = to!T(what); 4714 //*where = cast(T) stringToLong(what); 4715 } 4716 4717 /++ 4718 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4719 4720 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4721 4722 Note that this creates the widget but does not attach any event handlers to it. 4723 +/ 4724 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4725 4726 string displayName = __traits(identifier, tt).beautify; 4727 4728 static if(controlledByCount!tt == 1) { 4729 foreach(i, attr; __traits(getAttributes, tt)) { 4730 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4731 auto w = attr.construct(parent); 4732 static if(__traits(compiles, w.setPosition(*valptr))) 4733 update = () { w.setPosition(*valptr); }; 4734 else static if(__traits(compiles, w.setValue(*valptr))) 4735 update = () { w.setValue(*valptr); }; 4736 4737 if(update) 4738 update(); 4739 return w; 4740 } 4741 } 4742 } else static if(controlledByCount!tt == 0) { 4743 static if(is(typeof(tt) == enum)) { 4744 // FIXME: update 4745 auto dds = new DropDownSelection(parent); 4746 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4747 dds.addOption(option); 4748 if(__traits(getMember, typeof(tt), option) == *valptr) 4749 dds.setSelection(cast(int) idx); 4750 } 4751 return dds; 4752 } else static if(is(typeof(tt) == bool)) { 4753 auto box = new Checkbox(displayName, parent); 4754 update = () { box.isChecked = *valptr; }; 4755 update(); 4756 return box; 4757 } else static if(is(typeof(tt) : const long)) { 4758 auto le = new LabeledLineEdit(displayName, parent); 4759 update = () { le.content = toInternal!string(*valptr); }; 4760 update(); 4761 return le; 4762 } else static if(is(typeof(tt) : const double)) { 4763 auto le = new LabeledLineEdit(displayName, parent); 4764 import std.conv; 4765 update = () { le.content = to!string(*valptr); }; 4766 update(); 4767 return le; 4768 } else static if(is(typeof(tt) : const string)) { 4769 auto le = new LabeledLineEdit(displayName, parent); 4770 update = () { le.content = *valptr; }; 4771 update(); 4772 return le; 4773 } else static if(is(typeof(tt) == E[], E)) { 4774 auto w = new ArrayEditingWidget!E(parent); 4775 // FIXME update 4776 return w; 4777 } else static if(is(typeof(tt) == function)) { 4778 auto w = new Button(displayName, parent); 4779 return w; 4780 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4781 return parent.addDataControllerWidget(tt); 4782 } else static assert(0, typeof(tt).stringof); 4783 } else static assert(0, "multiple controllers not yet supported"); 4784 } 4785 4786 class ArrayEditingWidget(T) : ArrayEditingWidgetBase { 4787 this(Widget parent) { 4788 super(parent); 4789 } 4790 } 4791 4792 class ArrayEditingWidgetBase : Widget { 4793 this(Widget parent) { 4794 super(parent); 4795 4796 // FIXME: a trash can to move items into to delete them? 4797 static class MyListViewItem : GenericListViewItem { 4798 this(Widget parent) { 4799 super(parent); 4800 4801 /+ 4802 drag handle 4803 left click lets you move the whole selection. if the current element is not selected, it changes the selection to it. 4804 right click here gives you the movement controls too 4805 index/key view zone 4806 left click here selects/unselects 4807 element view/edit zone 4808 delete button 4809 +/ 4810 4811 // FIXME: make sure the index is viewable 4812 4813 auto hl = new HorizontalLayout(this); 4814 4815 button = new CommandButton("d", hl); 4816 4817 label = new TextLabel("unloaded", TextAlignment.Left, hl); 4818 // if member editable, have edit view... get from the subclass. 4819 4820 // or a "..." menu? 4821 button = new CommandButton("Up", hl); // shift+click is move to top 4822 button = new CommandButton("Down", hl); // shift+click is move to bottom 4823 button = new CommandButton("Move to", hl); // move before, after, or swap 4824 button = new CommandButton("Delete", hl); 4825 4826 button.addEventListener("triggered", delegate(){ 4827 //messageBox(text("clicked ", currentIndexLoaded())); 4828 }); 4829 } 4830 override void showItem(int idx) { 4831 label.label = "Item ";// ~ to!string(idx); 4832 } 4833 4834 TextLabel label; 4835 Button button; 4836 } 4837 4838 auto outer_this = this; 4839 4840 // FIXME: make sure item count is easy to see 4841 4842 glvw = new class GenericListViewWidget { 4843 this() { 4844 super(outer_this); 4845 } 4846 override GenericListViewItem itemFactory(Widget parent) { 4847 return new MyListViewItem(parent); 4848 } 4849 override Size itemSize() { 4850 return Size(0, scaleWithDpi(80)); 4851 } 4852 4853 override Menu contextMenu(int x, int y) { 4854 return createContextMenuFromAnnotatedCode(this); 4855 } 4856 4857 @context_menu { 4858 void Select_All() { 4859 4860 } 4861 4862 void Undo() { 4863 4864 } 4865 4866 void Redo() { 4867 4868 } 4869 4870 void Cut() { 4871 4872 } 4873 4874 void Copy() { 4875 4876 } 4877 4878 void Paste() { 4879 4880 } 4881 4882 void Delete() { 4883 4884 } 4885 4886 void Find() { 4887 4888 } 4889 } 4890 }; 4891 4892 glvw.setItemCount(400); 4893 4894 auto hl = new HorizontalLayout(this); 4895 add = new FreeEntrySelection(hl); 4896 addButton = new Button("Add", hl); 4897 } 4898 4899 GenericListViewWidget glvw; 4900 ComboboxBase add; 4901 Button addButton; 4902 /+ 4903 Controls: 4904 clear (select all / delete) 4905 reset (confirmation blocked button, maybe only on the whole form? or hit undo so many times to get back there) 4906 add item 4907 palette of options to add to the array (add prolly a combo box) 4908 rearrange - move up/down, drag and drop a selection? right click can always do, left click only drags when on a selection handle. 4909 edit/input/view items (GLVW? or it could be a table view in a way.) 4910 undo/redo 4911 select whole elements (even if a struct) 4912 cut/copy/paste elements 4913 4914 could have an element picker, a details pane, and an add bare? 4915 4916 4917 put a handle on the elements for left click dragging. allow right click drag anywhere but pretty big wiggle until it enables. 4918 left click and drag should never work for plain text, i more want to change selection there and there no room to put a handle on it. 4919 the handle should let dragging w/o changing the selection, or if part of the selection, drag the whole selection i think. 4920 make it textured and use the grabby hand mouse cursor. 4921 +/ 4922 } 4923 4924 /++ 4925 A button that pops up a menu on click for working on a particular item or selection. 4926 4927 History: 4928 Added March 23, 2025 4929 +/ 4930 class MenuPopupButton : Button { 4931 /++ 4932 You might consider using [createContextMenuFromAnnotatedCode] to populate the `menu` argument. 4933 4934 You also may want to set the [prepare] delegate after construction. 4935 +/ 4936 this(Menu menu, Widget parent) { 4937 assert(menu !is null); 4938 4939 this.menu = menu; 4940 super("...", parent); 4941 } 4942 4943 private Menu menu; 4944 /++ 4945 If set, this delegate is called before popping up the window. This gives you a chance 4946 to prepare your dynamic data structures for the element(s) selected. 4947 4948 For example, if your `MenuPopupButton` is attached to a [GenericListViewItem], you can call 4949 [GenericListViewItem.currentIndexLoaded] in here and set it to a variable in the object you 4950 called [createContextMenuFromAnnotatedCode] to apply the operation to the right object. 4951 4952 (The api could probably be simpler...) 4953 +/ 4954 void delegate() prepare; 4955 4956 override void defaultEventHandler_triggered(scope Event e) { 4957 if(prepare) 4958 prepare(); 4959 showContextMenu(this.x, this.y + this.height, -2, -2, menu); 4960 } 4961 4962 override int maxHeight() { 4963 return defaultLineHeight; 4964 } 4965 4966 override int maxWidth() { 4967 return defaultLineHeight; 4968 } 4969 } 4970 4971 /++ 4972 A button that pops up an information box, similar to a tooltip, but explicitly triggered. 4973 4974 FIXME: i want to be able to easily embed these in other things too. 4975 +/ 4976 class TipPopupButton : Button { 4977 /++ 4978 +/ 4979 this(Widget delegate(Widget p) factory, Widget parent) { 4980 this.factory = factory; 4981 super("?", parent); 4982 } 4983 /// ditto 4984 this(string tip, Widget parent) { 4985 this((parent) { 4986 auto td = new TextDisplayTooltip(tip, parent); 4987 return td; 4988 }, parent); 4989 } 4990 4991 private Widget delegate(Widget p) factory; 4992 4993 override void defaultEventHandler_triggered(scope Event e) { 4994 auto window = new TooltipWindow(factory, this); 4995 window.popup(this); 4996 } 4997 4998 private static class TextDisplayTooltip : TextDisplay { 4999 this(string txt, Widget parent) { 5000 super(txt, parent); 5001 } 5002 5003 // override int minHeight() { return defaultLineHeight; } 5004 // override int flexBasisHeight() { return defaultLineHeight; } 5005 5006 static class Style : TextDisplay.Style { 5007 override WidgetBackground background() { 5008 return WidgetBackground(Color.yellow); 5009 } 5010 5011 override FrameStyle borderStyle() { 5012 return FrameStyle.solid; 5013 } 5014 5015 override Color borderColor() { 5016 return Color.black; 5017 } 5018 } 5019 5020 mixin OverrideStyle!Style; 5021 } 5022 } 5023 5024 /++ 5025 History: 5026 Added March 23, 2025 5027 +/ 5028 class TooltipWindow : Window { 5029 5030 private Widget previouslyFocusedWidget; 5031 private Widget* previouslyFocusedWidgetBelongsIn; 5032 5033 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 5034 if(offsetY == int.min) 5035 offsetY = 0; 5036 5037 int w = child.flexBasisWidth(); 5038 int h = child.flexBasisHeight() + this.paddingTop + this.paddingBottom + /* horiz scroll bar - FIXME */ 16 + 2 /* for border */; 5039 5040 auto coord = parent.globalCoordinates(); 5041 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 5042 5043 this.width = w; 5044 this.height = h; 5045 5046 this.recomputeChildLayout(); 5047 5048 static if(UsingSimpledisplayX11) 5049 XSync(XDisplayConnection.get, 0); 5050 5051 dropDown.visibilityChanged = (bool visible) { 5052 if(visible) { 5053 this.redraw(); 5054 //dropDown.grabInput(); 5055 captureMouse(this); 5056 5057 if(previouslyFocusedWidget is null) 5058 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 5059 parent.parentWindow.focusedWidget = this; 5060 } else { 5061 releaseMouseCapture(); 5062 //dropDown.releaseInputGrab(); 5063 5064 parent.parentWindow.focusedWidget = previouslyFocusedWidget; 5065 5066 static if(UsingSimpledisplayX11) 5067 flushGui(); 5068 } 5069 }; 5070 5071 dropDown.show(); 5072 5073 clickListener = this.addEventListener((scope ClickEvent ev) { 5074 if(ev.target is this) { 5075 unpopup(); 5076 } 5077 }, true /* again for asap action */); 5078 } 5079 5080 private EventListener clickListener; 5081 5082 void unpopup() { 5083 mouseLastOver = mouseLastDownOn = null; 5084 dropDown.hide(); 5085 clickListener.disconnect(); 5086 } 5087 5088 override void defaultEventHandler_char(CharEvent ce) { 5089 if(ce.character == '\033') 5090 unpopup(); 5091 } 5092 5093 private SimpleWindow dropDown; 5094 private Widget child; 5095 5096 /// 5097 this(Widget delegate(Widget p) factory, Widget parent) { 5098 assert(parent); 5099 assert(parent.parentWindow); 5100 assert(parent.parentWindow.win); 5101 dropDown = new SimpleWindow( 5102 250, 40, 5103 null, OpenGlOptions.no, Resizability.fixedSize, 5104 WindowTypes.tooltip, 5105 WindowFlags.dontAutoShow, 5106 parent ? parent.parentWindow.win : null 5107 ); 5108 5109 super(dropDown); 5110 5111 child = factory(this); 5112 } 5113 } 5114 5115 private template controlledByCount(alias tt) { 5116 static int helper() { 5117 int count; 5118 foreach(i, attr; __traits(getAttributes, tt)) 5119 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 5120 count++; 5121 return count; 5122 } 5123 5124 enum controlledByCount = helper; 5125 } 5126 5127 /++ 5128 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 5129 5130 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 5131 5132 History: 5133 The `redrawOnChange` parameter was added on May 28, 2021. 5134 +/ 5135 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 5136 auto dcw = new DataControllerWidget!T(t, parent); 5137 initializeDataControllerWidget(dcw, redrawOnChange); 5138 return dcw; 5139 } 5140 5141 /// ditto 5142 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 5143 auto dcw = new DataControllerWidget!T(t, parent); 5144 initializeDataControllerWidget(dcw, redrawOnChange); 5145 return dcw; 5146 } 5147 5148 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 5149 if(redrawOnChange !is null) 5150 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 5151 } 5152 5153 /++ 5154 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. 5155 5156 History: 5157 Finalized on June 3, 2021 for the dub v10.0 release 5158 +/ 5159 struct StyleInformation { 5160 private Widget w; 5161 private BaseVisualTheme visualTheme; 5162 5163 private this(Widget w) { 5164 this.w = w; 5165 this.visualTheme = WidgetPainter.visualTheme; 5166 } 5167 5168 /++ 5169 Forwards to [Widget.Style] 5170 5171 Bugs: 5172 It is supposed to fall back to the [VisualTheme] if 5173 the style doesn't override the default, but that is 5174 not generally implemented. Many of them may end up 5175 being explicit overloads instead of the generic 5176 opDispatch fallback, like [font] is now. 5177 +/ 5178 public @property opDispatch(string name)() { 5179 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 5180 w.useStyleProperties((scope Widget.Style props) { 5181 //visualTheme.useStyleProperties(w, (props) { 5182 prop = __traits(getMember, props, name); 5183 }); 5184 return prop; 5185 } 5186 5187 /++ 5188 Returns the cached font object associated with the widget, 5189 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 5190 5191 History: 5192 Prior to March 21, 2022 (dub v10.7), `font` went through 5193 [opDispatch], which did not use the cache. You can now call it 5194 repeatedly without guilt. 5195 +/ 5196 public @property OperatingSystemFont font() { 5197 OperatingSystemFont prop; 5198 w.useStyleProperties((scope Widget.Style props) { 5199 prop = props.fontCached; 5200 }); 5201 if(prop is null) { 5202 prop = visualTheme.defaultFontCached(w.currentDpi); 5203 } 5204 return prop; 5205 } 5206 5207 @property { 5208 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 5209 /** */ int paddingLeft() { return w.paddingLeft(); } 5210 /** */ int paddingRight() { return w.paddingRight(); } 5211 /** */ int paddingTop() { return w.paddingTop(); } 5212 /** */ int paddingBottom() { return w.paddingBottom(); } 5213 5214 /** */ int marginLeft() { return w.marginLeft(); } 5215 /** */ int marginRight() { return w.marginRight(); } 5216 /** */ int marginTop() { return w.marginTop(); } 5217 /** */ int marginBottom() { return w.marginBottom(); } 5218 5219 /** */ int maxHeight() { return w.maxHeight(); } 5220 /** */ int minHeight() { return w.minHeight(); } 5221 5222 /** */ int maxWidth() { return w.maxWidth(); } 5223 /** */ int minWidth() { return w.minWidth(); } 5224 5225 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 5226 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 5227 5228 /** */ int heightStretchiness() { return w.heightStretchiness(); } 5229 /** */ int widthStretchiness() { return w.widthStretchiness(); } 5230 5231 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 5232 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 5233 5234 // Global helpers some of these are unstable. 5235 static: 5236 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 5237 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 5238 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 5239 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 5240 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 5241 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5242 5243 /** */ Color activeTabColor() { return lightAccentColor; } 5244 /** */ Color buttonColor() { return windowBackgroundColor; } 5245 /** */ Color depressedButtonColor() { return darkAccentColor; } 5246 /** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; } 5247 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 5248 auto c = WidgetPainter.visualTheme.selectionColor(); 5249 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 5250 } 5251 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5252 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5253 } 5254 5255 5256 5257 /+ 5258 5259 private static auto extractStyleProperty(string name)(Widget w) { 5260 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 5261 w.useStyleProperties((props) { 5262 prop = __traits(getMember, props, name); 5263 }); 5264 return prop; 5265 } 5266 5267 // FIXME: clear this upon a X server disconnect 5268 private static OperatingSystemFont[string] fontCache; 5269 5270 T getProperty(T)(string name, lazy T default_) { 5271 if(visualTheme !is null) { 5272 auto str = visualTheme.getPropertyString(w, name); 5273 if(str is null) 5274 return default_; 5275 static if(is(T == Color)) 5276 return Color.fromString(str); 5277 else static if(is(T == Measurement)) 5278 return Measurement(cast(int) toInternal!int(str)); 5279 else static if(is(T == WidgetBackground)) 5280 return WidgetBackground.fromString(str); 5281 else static if(is(T == OperatingSystemFont)) { 5282 if(auto f = str in fontCache) 5283 return *f; 5284 else 5285 return fontCache[str] = new OperatingSystemFont(str); 5286 } else static if(is(T == FrameStyle)) { 5287 switch(str) { 5288 default: 5289 return FrameStyle.none; 5290 foreach(style; __traits(allMembers, FrameStyle)) 5291 case style: 5292 return __traits(getMember, FrameStyle, style); 5293 } 5294 } else static assert(0); 5295 } else 5296 return default_; 5297 } 5298 5299 static struct Measurement { 5300 int value; 5301 alias value this; 5302 } 5303 5304 @property: 5305 5306 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 5307 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 5308 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 5309 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 5310 5311 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 5312 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 5313 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 5314 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 5315 5316 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 5317 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 5318 5319 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 5320 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 5321 5322 5323 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 5324 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 5325 5326 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 5327 5328 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 5329 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 5330 5331 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 5332 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 5333 5334 5335 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 5336 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 5337 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 5338 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 5339 5340 Color activeTabColor() { return lightAccentColor; } 5341 Color buttonColor() { return windowBackgroundColor; } 5342 Color depressedButtonColor() { return darkAccentColor; } 5343 Color hoveringColor() { return Color(228, 228, 228); } 5344 Color activeListXorColor() { 5345 auto c = WidgetPainter.visualTheme.selectionColor(); 5346 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 5347 } 5348 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 5349 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 5350 +/ 5351 } 5352 5353 5354 5355 // pragma(msg, __traits(classInstanceSize, Widget)); 5356 5357 /*private*/ template EventString(E) { 5358 static if(is(typeof(E.EventString))) 5359 enum EventString = E.EventString; 5360 else 5361 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 5362 } 5363 5364 /*private*/ template EventStringIdentifier(E) { 5365 string helper() { 5366 auto es = EventString!E; 5367 char[] id = new char[](es.length * 2); 5368 size_t idx; 5369 foreach(char ch; es) { 5370 id[idx++] = cast(char)('a' + (ch >> 4)); 5371 id[idx++] = cast(char)('a' + (ch & 0x0f)); 5372 } 5373 return cast(string) id; 5374 } 5375 5376 enum EventStringIdentifier = helper(); 5377 } 5378 5379 5380 template classStaticallyEmits(This, EventType) { 5381 static if(is(This Base == super)) 5382 static if(is(Base : Widget)) 5383 enum baseEmits = classStaticallyEmits!(Base, EventType); 5384 else 5385 enum baseEmits = false; 5386 else 5387 enum baseEmits = false; 5388 5389 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 5390 5391 enum classStaticallyEmits = thisEmits || baseEmits; 5392 } 5393 5394 /++ 5395 A helper to make widgets out of other native windows. 5396 5397 History: 5398 Factored out of OpenGlWidget on November 5, 2021 5399 +/ 5400 class NestedChildWindowWidget : Widget { 5401 SimpleWindow win; 5402 5403 /++ 5404 Used on X to send focus to the appropriate child window when requested by the window manager. 5405 5406 Normally returns its own nested window. Can also return another child or null to revert to the parent 5407 if you override it in a child class. 5408 5409 History: 5410 Added April 2, 2022 (dub v10.8) 5411 +/ 5412 SimpleWindow focusableWindow() { 5413 return win; 5414 } 5415 5416 /// 5417 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5418 this(SimpleWindow win, Widget parent) { 5419 this.parentWindow = parent.parentWindow; 5420 this.win = win; 5421 5422 super(parent); 5423 windowsetup(win); 5424 } 5425 5426 static protected SimpleWindow getParentWindow(Widget parent) { 5427 assert(parent !is null); 5428 SimpleWindow pwin = parent.parentWindow.win; 5429 5430 version(win32_widgets) { 5431 HWND phwnd; 5432 auto wtf = parent; 5433 while(wtf) { 5434 if(wtf.hwnd) { 5435 phwnd = wtf.hwnd; 5436 break; 5437 } 5438 wtf = wtf.parent; 5439 } 5440 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 5441 if(phwnd) 5442 pwin = new SimpleWindow(phwnd); 5443 } 5444 5445 return pwin; 5446 } 5447 5448 /++ 5449 Called upon the nested window being destroyed. 5450 Remember the window has already been destroyed at 5451 this point, so don't use the native handle for anything. 5452 5453 History: 5454 Added April 3, 2022 (dub v10.8) 5455 +/ 5456 protected void dispose() { 5457 5458 } 5459 5460 protected void windowsetup(SimpleWindow w) { 5461 /* 5462 win.onFocusChange = (bool getting) { 5463 if(getting) 5464 this.focus(); 5465 }; 5466 */ 5467 5468 /+ 5469 win.onFocusChange = (bool getting) { 5470 if(getting) { 5471 this.parentWindow.focusedWidget = this; 5472 this.emit!FocusEvent(); 5473 this.emit!FocusInEvent(); 5474 } else { 5475 this.emit!BlurEvent(); 5476 this.emit!FocusOutEvent(); 5477 } 5478 }; 5479 +/ 5480 5481 win.onDestroyed = () { 5482 this.dispose(); 5483 }; 5484 5485 version(win32_widgets) { 5486 Widget.nativeMapping[win.hwnd] = this; 5487 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 5488 } else { 5489 win.setEventHandlers( 5490 (MouseEvent e) { 5491 Widget p = this; 5492 while(p ! is parentWindow) { 5493 e.x += p.x; 5494 e.y += p.y; 5495 p = p.parent; 5496 } 5497 parentWindow.dispatchMouseEvent(e); 5498 }, 5499 (KeyEvent e) { 5500 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 5501 parentWindow.dispatchKeyEvent(e); 5502 }, 5503 (dchar e) { 5504 parentWindow.dispatchCharEvent(e); 5505 }, 5506 ); 5507 } 5508 5509 } 5510 5511 override bool showOrHideIfNativeWindow(bool shouldShow) { 5512 auto cur = hidden; 5513 win.hidden = !shouldShow; 5514 if(cur != shouldShow && shouldShow) 5515 redraw(); 5516 return true; 5517 } 5518 5519 /// OpenGL widgets cannot have child widgets. Do not call this. 5520 /* @disable */ final override void addChild(Widget, int) { 5521 throw new Error("cannot add children to OpenGL widgets"); 5522 } 5523 5524 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 5525 /// Keep in mind that events like mouse coordinates are still relative to your size. 5526 override void registerMovement() { 5527 // writefln("%d %d %d %d", x,y,width,height); 5528 version(win32_widgets) 5529 auto pos = getChildPositionRelativeToParentHwnd(this); 5530 else 5531 auto pos = getChildPositionRelativeToParentOrigin(this); 5532 win.moveResize(pos[0], pos[1], width, height); 5533 5534 registerMovementAdditionalWork(); 5535 sendResizeEvent(); 5536 } 5537 5538 abstract void registerMovementAdditionalWork(); 5539 } 5540 5541 /++ 5542 Nests an opengl capable window inside this window as a widget. 5543 5544 You may also just want to create an additional [SimpleWindow] with 5545 [OpenGlOptions.yes] yourself. 5546 5547 An OpenGL widget cannot have child widgets. It will throw if you try. 5548 +/ 5549 static if(OpenGlEnabled) 5550 class OpenGlWidget : NestedChildWindowWidget { 5551 5552 override void registerMovementAdditionalWork() { 5553 win.setAsCurrentOpenGlContext(); 5554 } 5555 5556 /// 5557 this(Widget parent) { 5558 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5559 super(win, parent); 5560 } 5561 5562 override void paint(WidgetPainter painter) { 5563 win.setAsCurrentOpenGlContext(); 5564 glViewport(0, 0, this.width, this.height); 5565 win.redrawOpenGlSceneNow(); 5566 } 5567 5568 void redrawOpenGlScene(void delegate() dg) { 5569 win.redrawOpenGlScene = dg; 5570 } 5571 } 5572 5573 /++ 5574 This demo shows how to draw text in an opengl scene. 5575 +/ 5576 unittest { 5577 import arsd.minigui; 5578 import arsd.ttf; 5579 5580 void main() { 5581 auto window = new Window(); 5582 5583 auto widget = new OpenGlWidget(window); 5584 5585 // old means non-shader code so compatible with glBegin etc. 5586 // tbh I haven't implemented new one in font yet... 5587 // anyway, declaring here, will construct soon. 5588 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 5589 5590 // this is a little bit awkward, calling some methods through 5591 // the underlying SimpleWindow `win` method, and you can't do this 5592 // on a nanovega widget due to conflicts so I should probably fix 5593 // the api to be a bit easier. But here it will work. 5594 // 5595 // Alternatively, you could load the font on the first draw, inside 5596 // the redrawOpenGlScene, and keep a flag so you don't do it every 5597 // time. That'd be a bit easier since the lib sets up the context 5598 // by then guaranteed. 5599 // 5600 // But still, I wanna show this. 5601 widget.win.visibleForTheFirstTime = delegate { 5602 // must set the opengl context 5603 widget.win.setAsCurrentOpenGlContext(); 5604 5605 // if you were doing a OpenGL 3+ shader, this 5606 // gets especially important to do in order. With 5607 // old-style opengl, I think you can even do it 5608 // in main(), but meh, let's show it more correctly. 5609 5610 // Anyway, now it is time to load the font from the 5611 // OS (you can alternatively load one from a .ttf file 5612 // you bundle with the application), then load the 5613 // font into texture for drawing. 5614 5615 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 5616 5617 assert(!osfont.isNull()); // make sure it actually loaded 5618 5619 // using typeof to avoid repeating the long name lol 5620 glfont = new typeof(glfont)( 5621 // get the raw data from the font for loading in here 5622 // since it doesn't use the OS function to draw the 5623 // text, we gotta treat it more as a file than as 5624 // a drawing api. 5625 osfont.getTtfBytes(), 5626 18, // need to respecify size since opengl world is different coordinate system 5627 5628 // these last two numbers are why it is called 5629 // "Limited" font. It only loads the characters 5630 // in the given range, since the texture atlas 5631 // it references is all a big image generated ahead 5632 // of time. You could maybe do the whole thing but 5633 // idk how much memory that is. 5634 // 5635 // But here, 0-128 represents the ASCII range, so 5636 // good enough for most English things, numeric labels, 5637 // etc. 5638 0, 5639 128 5640 ); 5641 }; 5642 5643 widget.redrawOpenGlScene = () { 5644 // now we can use the glfont's drawString function 5645 5646 // first some opengl setup. You can do this in one place 5647 // on window first visible too in many cases, just showing 5648 // here cuz it is easier for me. 5649 5650 // gonna need some alpha blending or it just looks awful 5651 glEnable(GL_BLEND); 5652 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5653 glClearColor(0,0,0,0); 5654 glDepthFunc(GL_LEQUAL); 5655 5656 // Also need to enable 2d textures, since it draws the 5657 // font characters as images baked in 5658 glMatrixMode(GL_MODELVIEW); 5659 glLoadIdentity(); 5660 glDisable(GL_DEPTH_TEST); 5661 glEnable(GL_TEXTURE_2D); 5662 5663 // the orthographic matrix is best for 2d things like text 5664 // so let's set that up. This matrix makes the coordinates 5665 // in the opengl scene be one-to-one with the actual pixels 5666 // on screen. (Not necessarily best, you may wish to scale 5667 // things, but it does help keep fonts looking normal.) 5668 glMatrixMode(GL_PROJECTION); 5669 glLoadIdentity(); 5670 glOrtho(0, widget.width, widget.height, 0, 0, 1); 5671 5672 // you can do other glScale, glRotate, glTranslate, etc 5673 // to the matrix here of course if you want. 5674 5675 // note the x,y coordinates here are for the text baseline 5676 // NOT the upper-left corner. The baseline is like the line 5677 // in the notebook you write on. Most the letters are actually 5678 // above it, but some, like p and q, dip a bit below it. 5679 // 5680 // So if you're used to the upper left coordinate like the 5681 // rest of simpledisplay/minigui usually do, do the 5682 // y + glfont.ascent to bring it down a little. So this 5683 // example puts the string in the upper left of the window. 5684 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 5685 5686 // re color btw: the function sets a solid color internally, 5687 // but you actually COULD do your own thing for rainbow effects 5688 // and the sort if you wanted too, by pulling its guts out. 5689 // Just view its source for an idea of how it actually draws: 5690 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 5691 5692 // it gets a bit complicated with the character positioning, 5693 // but the opengl parts are fairly simple: bind a texture, 5694 // set the color, draw a quad for each letter. 5695 5696 5697 // the last optional argument there btw is a bounding box 5698 // it will/ use to word wrap and return an object you can 5699 // use to implement scrolling or pagination; it tells how 5700 // much of the string didn't fit in the box. But for simple 5701 // labels we can just ignore that. 5702 5703 5704 // I'd suggest drawing text as the last step, after you 5705 // do your other drawing. You might use the push/pop matrix 5706 // stuff to keep your place. You, in theory, should be able 5707 // to do text in a 3d space but I've never actually tried 5708 // that.... 5709 }; 5710 5711 window.loop(); 5712 } 5713 } 5714 5715 version(custom_widgets) 5716 private class TextListViewWidget : GenericListViewWidget { 5717 static class TextListViewItem : GenericListViewItem { 5718 ListWidget controller; 5719 this(ListWidget controller, Widget parent) { 5720 this.controller = controller; 5721 this.tabStop = false; 5722 super(parent); 5723 } 5724 5725 ListWidget.Option* showing; 5726 5727 override void showItem(int idx) { 5728 showing = idx < controller.options.length ? &controller.options[idx] : null; 5729 redraw(); // is this necessary? the generic thing might call it... 5730 } 5731 5732 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 5733 if(showing is null) 5734 return bounds; 5735 painter.drawText(bounds.upperLeft, showing.label); 5736 return bounds; 5737 } 5738 5739 static class Style : Widget.Style { 5740 override WidgetBackground background() { 5741 // FIXME: change it if it is focused or not 5742 // 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 5743 auto tlvi = cast(TextListViewItem) widget; 5744 if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected) 5745 return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode 5746 return super.background(); 5747 } 5748 5749 override Color foregroundColor() { 5750 auto tlvi = cast(TextListViewItem) widget; 5751 return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor(); 5752 } 5753 5754 override FrameStyle outlineStyle() { 5755 // FIXME: change it if it is focused or not 5756 auto tlvi = cast(TextListViewItem) widget; 5757 return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle(); 5758 } 5759 } 5760 mixin OverrideStyle!Style; 5761 5762 mixin Padding!q{2}; 5763 5764 override void defaultEventHandler_click(ClickEvent event) { 5765 if(event.button == MouseButton.left) { 5766 controller.setSelection(currentIndexLoaded()); 5767 controller.focusOn = currentIndexLoaded(); 5768 } 5769 } 5770 5771 } 5772 5773 ListWidget controller; 5774 5775 this(ListWidget parent) { 5776 this.controller = parent; 5777 this.tabStop = false; // this is only used as a child of the ListWidget 5778 super(parent); 5779 5780 smw.movementPerButtonClick(1, itemSize().height); 5781 } 5782 5783 override Size itemSize() { 5784 return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */)); 5785 } 5786 5787 override GenericListViewItem itemFactory(Widget parent) { 5788 return new TextListViewItem(controller, parent); 5789 } 5790 5791 static class Style : Widget.Style { 5792 override FrameStyle borderStyle() { 5793 return FrameStyle.sunk; 5794 } 5795 5796 override WidgetBackground background() { 5797 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 5798 } 5799 } 5800 mixin OverrideStyle!Style; 5801 } 5802 5803 /++ 5804 A list widget contains a list of strings that the user can examine and select. 5805 5806 5807 In the future, items in the list may be possible to be more than just strings. 5808 5809 See_Also: 5810 [TableView] 5811 +/ 5812 class ListWidget : Widget { 5813 /// 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. 5814 mixin Emits!(ChangeEvent!void); 5815 5816 version(custom_widgets) 5817 TextListViewWidget glvw; 5818 5819 static struct Option { 5820 string label; 5821 bool selected; 5822 void* tag; 5823 } 5824 private Option[] options; 5825 5826 /++ 5827 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 5828 +/ 5829 void setSelection(int y) { 5830 if(!multiSelect) 5831 foreach(ref opt; options) 5832 opt.selected = false; 5833 if(y >= 0 && y < options.length) 5834 options[y].selected = !options[y].selected; 5835 5836 version(custom_widgets) 5837 focusOn = y; 5838 5839 this.emit!(ChangeEvent!void)(delegate {}); 5840 5841 version(custom_widgets) 5842 redraw(); 5843 } 5844 5845 /++ 5846 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 5847 Returns -1 if nothing is selected. 5848 +/ 5849 int getSelection() 5850 { 5851 foreach(i, opt; options) { 5852 if (opt.selected) 5853 return cast(int) i; 5854 } 5855 return -1; 5856 } 5857 5858 version(custom_widgets) 5859 private int focusOn; 5860 5861 this(Widget parent) { 5862 super(parent); 5863 5864 version(custom_widgets) 5865 glvw = new TextListViewWidget(this); 5866 5867 version(win32_widgets) 5868 createWin32Window(this, WC_LISTBOX, "", 5869 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 5870 } 5871 5872 version(win32_widgets) 5873 override void handleWmCommand(ushort code, ushort id) { 5874 switch(code) { 5875 case LBN_SELCHANGE: 5876 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 5877 setSelection(cast(int) sel); 5878 break; 5879 default: 5880 } 5881 } 5882 5883 5884 void addOption(string text, void* tag = null) { 5885 options ~= Option(text, false, tag); 5886 version(win32_widgets) { 5887 WCharzBuffer buffer = WCharzBuffer(text); 5888 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 5889 } 5890 version(custom_widgets) { 5891 glvw.setItemCount(cast(int) options.length); 5892 //setContentSize(width, cast(int) (options.length * defaultLineHeight)); 5893 redraw(); 5894 } 5895 } 5896 5897 void clear() { 5898 options = null; 5899 version(win32_widgets) { 5900 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 5901 {} 5902 5903 } else version(custom_widgets) { 5904 focusOn = -1; 5905 glvw.setItemCount(0); 5906 redraw(); 5907 } 5908 } 5909 5910 version(custom_widgets) 5911 override void defaultEventHandler_keydown(KeyDownEvent kde) { 5912 void changedFocusOn() { 5913 scrollFocusIntoView(); 5914 if(multiSelect) 5915 redraw(); 5916 else 5917 setSelection(focusOn); 5918 } 5919 switch(kde.key) { 5920 case Key.Up: 5921 if(focusOn) { 5922 focusOn--; 5923 changedFocusOn(); 5924 } 5925 break; 5926 case Key.Down: 5927 if(focusOn + 1 < options.length) { 5928 focusOn++; 5929 changedFocusOn(); 5930 } 5931 break; 5932 case Key.Home: 5933 if(focusOn) { 5934 focusOn = 0; 5935 changedFocusOn(); 5936 } 5937 break; 5938 case Key.End: 5939 if(options.length && focusOn + 1 != options.length) { 5940 focusOn = cast(int) options.length - 1; 5941 changedFocusOn(); 5942 } 5943 break; 5944 case Key.PageUp: 5945 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5946 focusOn -= n; 5947 if(focusOn < 0) 5948 focusOn = 0; 5949 changedFocusOn(); 5950 break; 5951 case Key.PageDown: 5952 if(options.length == 0) 5953 break; 5954 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5955 focusOn += n; 5956 if(focusOn >= options.length) 5957 focusOn = cast(int) options.length - 1; 5958 changedFocusOn(); 5959 break; 5960 5961 default: 5962 } 5963 } 5964 5965 version(custom_widgets) 5966 override void defaultEventHandler_char(CharEvent ce) { 5967 if(ce.character == '\n' || ce.character == ' ') { 5968 setSelection(focusOn); 5969 } else { 5970 // search for the item that best matches and jump to it 5971 // FIXME this sucks in tons of ways. the normal thing toolkits 5972 // do here is to search for a substring on a timer, but i'd kinda 5973 // rather make an actual little dialog with some options. still meh for now. 5974 dchar search = ce.character; 5975 if(search >= 'A' && search <= 'Z') 5976 search += 32; 5977 foreach(idx, option; options) { 5978 auto ch = option.label.length ? option.label[0] : 0; 5979 if(ch >= 'A' && ch <= 'Z') 5980 ch += 32; 5981 if(ch == search) { 5982 setSelection(cast(int) idx); 5983 scrollSelectionIntoView(); 5984 break; 5985 } 5986 } 5987 5988 } 5989 } 5990 5991 version(win32_widgets) 5992 enum multiSelect = false; /// not implemented yet 5993 else 5994 bool multiSelect; 5995 5996 override int heightStretchiness() { return 6; } 5997 5998 version(custom_widgets) 5999 void scrollFocusIntoView() { 6000 glvw.ensureItemVisibleInScroll(focusOn); 6001 } 6002 6003 void scrollSelectionIntoView() { 6004 // FIXME: implement on Windows 6005 6006 version(custom_widgets) 6007 glvw.ensureItemVisibleInScroll(getSelection()); 6008 } 6009 6010 /* 6011 version(custom_widgets) 6012 override void defaultEventHandler_focusout(Event foe) { 6013 glvw.redraw(); 6014 } 6015 6016 version(custom_widgets) 6017 override void defaultEventHandler_focusin(Event foe) { 6018 glvw.redraw(); 6019 } 6020 */ 6021 6022 } 6023 6024 6025 6026 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 6027 /// NEVER USED 6028 enum ScrollBarShowPolicy { 6029 automatic, /// automatically show the scroll bar if it is necessary 6030 never, /// never show the scroll bar (scrolling must be done programmatically) 6031 always /// always show the scroll bar, even if it is disabled 6032 } 6033 6034 /++ 6035 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 6036 6037 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 6038 +/ 6039 // FIXME ScrollBarShowPolicy 6040 // FIXME: use the ScrollMessageWidget in here now that it exists 6041 deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it 6042 class ScrollableWidget : Widget { 6043 // FIXME: make line size configurable 6044 // FIXME: add keyboard controls 6045 version(win32_widgets) { 6046 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 6047 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 6048 auto pos = HIWORD(wParam); 6049 auto m = LOWORD(wParam); 6050 6051 // FIXME: I can reintroduce the 6052 // scroll bars now by using this 6053 // in the top-level window handler 6054 // to forward comamnds 6055 auto scrollbarHwnd = lParam; 6056 switch(m) { 6057 case SB_BOTTOM: 6058 if(msg == WM_HSCROLL) 6059 horizontalScrollTo(contentWidth_); 6060 else 6061 verticalScrollTo(contentHeight_); 6062 break; 6063 case SB_TOP: 6064 if(msg == WM_HSCROLL) 6065 horizontalScrollTo(0); 6066 else 6067 verticalScrollTo(0); 6068 break; 6069 case SB_ENDSCROLL: 6070 // idk 6071 break; 6072 case SB_LINEDOWN: 6073 if(msg == WM_HSCROLL) 6074 horizontalScroll(scaleWithDpi(16)); 6075 else 6076 verticalScroll(scaleWithDpi(16)); 6077 break; 6078 case SB_LINEUP: 6079 if(msg == WM_HSCROLL) 6080 horizontalScroll(scaleWithDpi(-16)); 6081 else 6082 verticalScroll(scaleWithDpi(-16)); 6083 break; 6084 case SB_PAGEDOWN: 6085 if(msg == WM_HSCROLL) 6086 horizontalScroll(scaleWithDpi(100)); 6087 else 6088 verticalScroll(scaleWithDpi(100)); 6089 break; 6090 case SB_PAGEUP: 6091 if(msg == WM_HSCROLL) 6092 horizontalScroll(scaleWithDpi(-100)); 6093 else 6094 verticalScroll(scaleWithDpi(-100)); 6095 break; 6096 case SB_THUMBPOSITION: 6097 case SB_THUMBTRACK: 6098 if(msg == WM_HSCROLL) 6099 horizontalScrollTo(pos); 6100 else 6101 verticalScrollTo(pos); 6102 6103 if(m == SB_THUMBTRACK) { 6104 // the event loop doesn't seem to carry on with a requested redraw.. 6105 // so we request it to get our dirty bit set... 6106 redraw(); 6107 6108 // then we need to immediately actually redraw it too for instant feedback to user 6109 6110 SimpleWindow.processAllCustomEvents(); 6111 //if(parentWindow) 6112 //parentWindow.actualRedraw(); 6113 } 6114 break; 6115 default: 6116 } 6117 } 6118 return super.hookedWndProc(msg, wParam, lParam); 6119 } 6120 } 6121 /// 6122 this(Widget parent) { 6123 this.parentWindow = parent.parentWindow; 6124 6125 version(win32_widgets) { 6126 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 6127 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 6128 super(parent); 6129 } else version(custom_widgets) { 6130 outerContainer = new InternalScrollableContainerWidget(this, parent); 6131 super(outerContainer); 6132 } else static assert(0); 6133 } 6134 6135 version(custom_widgets) 6136 InternalScrollableContainerWidget outerContainer; 6137 6138 override void defaultEventHandler_click(ClickEvent event) { 6139 if(event.button == MouseButton.wheelUp) 6140 verticalScroll(scaleWithDpi(-16)); 6141 if(event.button == MouseButton.wheelDown) 6142 verticalScroll(scaleWithDpi(16)); 6143 super.defaultEventHandler_click(event); 6144 } 6145 6146 override void defaultEventHandler_keydown(KeyDownEvent event) { 6147 switch(event.key) { 6148 case Key.Left: 6149 horizontalScroll(scaleWithDpi(-16)); 6150 break; 6151 case Key.Right: 6152 horizontalScroll(scaleWithDpi(16)); 6153 break; 6154 case Key.Up: 6155 verticalScroll(scaleWithDpi(-16)); 6156 break; 6157 case Key.Down: 6158 verticalScroll(scaleWithDpi(16)); 6159 break; 6160 case Key.Home: 6161 verticalScrollTo(0); 6162 break; 6163 case Key.End: 6164 verticalScrollTo(contentHeight); 6165 break; 6166 case Key.PageUp: 6167 verticalScroll(scaleWithDpi(-160)); 6168 break; 6169 case Key.PageDown: 6170 verticalScroll(scaleWithDpi(160)); 6171 break; 6172 default: 6173 } 6174 super.defaultEventHandler_keydown(event); 6175 } 6176 6177 6178 version(win32_widgets) 6179 override void recomputeChildLayout() { 6180 super.recomputeChildLayout(); 6181 SCROLLINFO info; 6182 info.cbSize = info.sizeof; 6183 info.nPage = viewportHeight; 6184 info.fMask = SIF_PAGE | SIF_RANGE; 6185 info.nMin = 0; 6186 info.nMax = contentHeight_; 6187 SetScrollInfo(hwnd, SB_VERT, &info, true); 6188 6189 info.cbSize = info.sizeof; 6190 info.nPage = viewportWidth; 6191 info.fMask = SIF_PAGE | SIF_RANGE; 6192 info.nMin = 0; 6193 info.nMax = contentWidth_; 6194 SetScrollInfo(hwnd, SB_HORZ, &info, true); 6195 } 6196 6197 /* 6198 Scrolling 6199 ------------ 6200 6201 You are assigned a width and a height by the layout engine, which 6202 is your viewport box. However, you may draw more than that by setting 6203 a contentWidth and contentHeight. 6204 6205 If these can be contained by the viewport, no scrollbar is displayed. 6206 If they cannot fit though, it will automatically show scroll as necessary. 6207 6208 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 6209 is zero, no vertical scrolling is performed. 6210 6211 If scrolling is necessary, the lib will automatically work with the bars. 6212 When you redraw, the origin and clipping info in the painter is set so if 6213 you just draw everything, it will work, but you can be more efficient by checking 6214 the viewportWidth, viewportHeight, and scrollOrigin members. 6215 */ 6216 6217 /// 6218 final @property int viewportWidth() { 6219 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 6220 } 6221 /// 6222 final @property int viewportHeight() { 6223 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 6224 } 6225 6226 // FIXME property 6227 Point scrollOrigin_; 6228 6229 /// 6230 final const(Point) scrollOrigin() { 6231 return scrollOrigin_; 6232 } 6233 6234 // the user sets these two 6235 private int contentWidth_ = 0; 6236 private int contentHeight_ = 0; 6237 6238 /// 6239 int contentWidth() { return contentWidth_; } 6240 /// 6241 int contentHeight() { return contentHeight_; } 6242 6243 /// 6244 void setContentSize(int width, int height) { 6245 contentWidth_ = width; 6246 contentHeight_ = height; 6247 6248 version(custom_widgets) { 6249 if(showingVerticalScroll || showingHorizontalScroll) { 6250 outerContainer.queueRecomputeChildLayout(); 6251 } 6252 6253 if(showingVerticalScroll()) 6254 outerContainer.verticalScrollBar.redraw(); 6255 if(showingHorizontalScroll()) 6256 outerContainer.horizontalScrollBar.redraw(); 6257 } else version(win32_widgets) { 6258 queueRecomputeChildLayout(); 6259 } else static assert(0); 6260 } 6261 6262 /// 6263 void verticalScroll(int delta) { 6264 verticalScrollTo(scrollOrigin.y + delta); 6265 } 6266 /// 6267 void verticalScrollTo(int pos) { 6268 scrollOrigin_.y = pos; 6269 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 6270 scrollOrigin_.y = contentHeight - viewportHeight; 6271 6272 if(scrollOrigin_.y < 0) 6273 scrollOrigin_.y = 0; 6274 6275 version(win32_widgets) { 6276 SCROLLINFO info; 6277 info.cbSize = info.sizeof; 6278 info.fMask = SIF_POS; 6279 info.nPos = scrollOrigin_.y; 6280 SetScrollInfo(hwnd, SB_VERT, &info, true); 6281 } else version(custom_widgets) { 6282 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 6283 } else static assert(0); 6284 6285 redraw(); 6286 } 6287 6288 /// 6289 void horizontalScroll(int delta) { 6290 horizontalScrollTo(scrollOrigin.x + delta); 6291 } 6292 /// 6293 void horizontalScrollTo(int pos) { 6294 scrollOrigin_.x = pos; 6295 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 6296 scrollOrigin_.x = contentWidth - viewportWidth; 6297 6298 if(scrollOrigin_.x < 0) 6299 scrollOrigin_.x = 0; 6300 6301 version(win32_widgets) { 6302 SCROLLINFO info; 6303 info.cbSize = info.sizeof; 6304 info.fMask = SIF_POS; 6305 info.nPos = scrollOrigin_.x; 6306 SetScrollInfo(hwnd, SB_HORZ, &info, true); 6307 } else version(custom_widgets) { 6308 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 6309 } else static assert(0); 6310 6311 redraw(); 6312 } 6313 /// 6314 void scrollTo(Point p) { 6315 verticalScrollTo(p.y); 6316 horizontalScrollTo(p.x); 6317 } 6318 6319 /// 6320 void ensureVisibleInScroll(Point p) { 6321 auto rect = viewportRectangle(); 6322 if(rect.contains(p)) 6323 return; 6324 if(p.x < rect.left) 6325 horizontalScroll(p.x - rect.left); 6326 else if(p.x > rect.right) 6327 horizontalScroll(p.x - rect.right); 6328 6329 if(p.y < rect.top) 6330 verticalScroll(p.y - rect.top); 6331 else if(p.y > rect.bottom) 6332 verticalScroll(p.y - rect.bottom); 6333 } 6334 6335 /// 6336 void ensureVisibleInScroll(Rectangle rect) { 6337 ensureVisibleInScroll(rect.upperLeft); 6338 ensureVisibleInScroll(rect.lowerRight); 6339 } 6340 6341 /// 6342 Rectangle viewportRectangle() { 6343 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 6344 } 6345 6346 /// 6347 bool showingHorizontalScroll() { 6348 return contentWidth > width; 6349 } 6350 /// 6351 bool showingVerticalScroll() { 6352 return contentHeight > height; 6353 } 6354 6355 /// This is called before the ordinary paint delegate, 6356 /// giving you a chance to draw the window frame, etc, 6357 /// before the scroll clip takes effect 6358 void paintFrameAndBackground(WidgetPainter painter) { 6359 version(win32_widgets) { 6360 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 6361 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 6362 // since the pen is null, to fill the whole space, we need the +1 on both. 6363 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 6364 SelectObject(painter.impl.hdc, p); 6365 SelectObject(painter.impl.hdc, b); 6366 } 6367 6368 } 6369 6370 // make space for the scroll bar, and that's it. 6371 final override int paddingRight() { return scaleWithDpi(16); } 6372 final override int paddingBottom() { return scaleWithDpi(16); } 6373 6374 /* 6375 END SCROLLING 6376 */ 6377 6378 override WidgetPainter draw() { 6379 int x = this.x, y = this.y; 6380 auto parent = this.parent; 6381 while(parent) { 6382 x += parent.x; 6383 y += parent.y; 6384 parent = parent.parent; 6385 } 6386 6387 //version(win32_widgets) { 6388 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6389 //} else { 6390 auto painter = parentWindow.win.draw(true); 6391 //} 6392 painter.originX = x; 6393 painter.originY = y; 6394 6395 painter.originX = painter.originX - scrollOrigin.x; 6396 painter.originY = painter.originY - scrollOrigin.y; 6397 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 6398 6399 return WidgetPainter(painter, this); 6400 } 6401 6402 override void addScrollPosition(ref int x, ref int y) { 6403 x += scrollOrigin.x; 6404 y += scrollOrigin.y; 6405 } 6406 6407 mixin ScrollableChildren; 6408 } 6409 6410 // you need to have a Point scrollOrigin in the class somewhere 6411 // and a paintFrameAndBackground 6412 private mixin template ScrollableChildren() { 6413 static assert(!__traits(isSame, this.addScrollPosition, Widget.addScrollPosition), "Your widget should provide `Point scrollOrigin()` and `override void addScrollPosition`"); 6414 6415 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6416 if(hidden) 6417 return; 6418 6419 //version(win32_widgets) 6420 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6421 6422 painter.originX = lox + x; 6423 painter.originY = loy + y; 6424 6425 bool actuallyPainted = false; 6426 6427 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 6428 if(clip == Rectangle.init) 6429 return; 6430 6431 if(force || redrawRequested) { 6432 //painter.setClipRectangle(scrollOrigin, width, height); 6433 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6434 paintFrameAndBackground(painter); 6435 } 6436 6437 /+ 6438 version(win32_widgets) { 6439 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 6440 } 6441 +/ 6442 6443 painter.originX = painter.originX - scrollOrigin.x; 6444 painter.originY = painter.originY - scrollOrigin.y; 6445 if(force || redrawRequested) { 6446 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 6447 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 6448 6449 //erase(painter); // we paintFrameAndBackground above so no need 6450 if(painter.visualTheme) 6451 painter.visualTheme.doPaint(this, painter); 6452 else 6453 paint(painter); 6454 6455 if(invalidate) { 6456 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6457 // children are contained inside this, so no need to do extra work 6458 invalidate = false; 6459 } 6460 6461 6462 actuallyPainted = true; 6463 redrawRequested = false; 6464 } 6465 6466 foreach(child; children) { 6467 if(cast(FixedPosition) child) 6468 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6469 else 6470 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6471 } 6472 } 6473 } 6474 6475 private class InternalScrollableContainerInsideWidget : ContainerWidget { 6476 ScrollableContainerWidget scw; 6477 6478 this(ScrollableContainerWidget parent) { 6479 scw = parent; 6480 super(parent); 6481 } 6482 6483 version(custom_widgets) 6484 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6485 if(hidden) 6486 return; 6487 6488 bool actuallyPainted = false; 6489 6490 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 6491 6492 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 6493 if(clip == Rectangle.init) 6494 return; 6495 6496 painter.originX = lox + x - scrollOrigin.x; 6497 painter.originY = loy + y - scrollOrigin.y; 6498 if(force || redrawRequested) { 6499 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6500 6501 erase(painter); 6502 if(painter.visualTheme) 6503 painter.visualTheme.doPaint(this, painter); 6504 else 6505 paint(painter); 6506 6507 if(invalidate) { 6508 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6509 // children are contained inside this, so no need to do extra work 6510 invalidate = false; 6511 } 6512 6513 actuallyPainted = true; 6514 redrawRequested = false; 6515 } 6516 foreach(child; children) { 6517 if(cast(FixedPosition) child) 6518 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6519 else 6520 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6521 } 6522 } 6523 6524 version(custom_widgets) 6525 override protected void addScrollPosition(ref int x, ref int y) { 6526 x += scw.scrollX_; 6527 y += scw.scrollY_; 6528 } 6529 } 6530 6531 /++ 6532 A widget meant to contain other widgets that may need to scroll. 6533 6534 Currently buggy. 6535 6536 History: 6537 Added July 1, 2021 (dub v10.2) 6538 6539 On January 3, 2022, I tried to use it in a few other cases 6540 and found it only worked well in the original test case. Since 6541 it still sucks, I think I'm going to rewrite it again. 6542 +/ 6543 class ScrollableContainerWidget : ContainerWidget { 6544 /// 6545 this(Widget parent) { 6546 super(parent); 6547 6548 container = new InternalScrollableContainerInsideWidget(this); 6549 hsb = new HorizontalScrollbar(this); 6550 vsb = new VerticalScrollbar(this); 6551 6552 tabStop = false; 6553 container.tabStop = false; 6554 magic = true; 6555 6556 6557 vsb.addEventListener("scrolltonextline", () { 6558 scrollBy(0, scaleWithDpi(16)); 6559 }); 6560 vsb.addEventListener("scrolltopreviousline", () { 6561 scrollBy(0,scaleWithDpi( -16)); 6562 }); 6563 vsb.addEventListener("scrolltonextpage", () { 6564 scrollBy(0, container.height); 6565 }); 6566 vsb.addEventListener("scrolltopreviouspage", () { 6567 scrollBy(0, -container.height); 6568 }); 6569 vsb.addEventListener((scope ScrollToPositionEvent spe) { 6570 scrollTo(scrollX_, spe.value); 6571 }); 6572 6573 this.addEventListener(delegate (scope ClickEvent e) { 6574 if(e.button == MouseButton.wheelUp) { 6575 if(!e.defaultPrevented) 6576 scrollBy(0, scaleWithDpi(-16)); 6577 e.stopPropagation(); 6578 } else if(e.button == MouseButton.wheelDown) { 6579 if(!e.defaultPrevented) 6580 scrollBy(0, scaleWithDpi(16)); 6581 e.stopPropagation(); 6582 } 6583 }); 6584 } 6585 6586 /+ 6587 override void defaultEventHandler_click(ClickEvent e) { 6588 } 6589 +/ 6590 6591 override void removeAllChildren() { 6592 container.removeAllChildren(); 6593 } 6594 6595 void scrollTo(int x, int y) { 6596 scrollBy(x - scrollX_, y - scrollY_); 6597 } 6598 6599 void scrollBy(int x, int y) { 6600 auto ox = scrollX_; 6601 auto oy = scrollY_; 6602 6603 auto nx = ox + x; 6604 auto ny = oy + y; 6605 6606 if(nx < 0) 6607 nx = 0; 6608 if(ny < 0) 6609 ny = 0; 6610 6611 auto maxX = hsb.max - container.width; 6612 if(maxX < 0) maxX = 0; 6613 auto maxY = vsb.max - container.height; 6614 if(maxY < 0) maxY = 0; 6615 6616 if(nx > maxX) 6617 nx = maxX; 6618 if(ny > maxY) 6619 ny = maxY; 6620 6621 auto dx = nx - ox; 6622 auto dy = ny - oy; 6623 6624 if(dx || dy) { 6625 version(win32_widgets) 6626 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 6627 else { 6628 redraw(); 6629 } 6630 6631 hsb.setPosition = nx; 6632 vsb.setPosition = ny; 6633 6634 scrollX_ = nx; 6635 scrollY_ = ny; 6636 } 6637 } 6638 6639 private int scrollX_; 6640 private int scrollY_; 6641 6642 void setTotalArea(int width, int height) { 6643 hsb.setMax(width); 6644 vsb.setMax(height); 6645 } 6646 6647 /// 6648 void setViewableArea(int width, int height) { 6649 hsb.setViewableArea(width); 6650 vsb.setViewableArea(height); 6651 } 6652 6653 private bool magic; 6654 override void addChild(Widget w, int position = int.max) { 6655 if(magic) 6656 container.addChild(w, position); 6657 else 6658 super.addChild(w, position); 6659 } 6660 6661 override void recomputeChildLayout() { 6662 if(hsb is null || vsb is null || container is null) return; 6663 6664 /+ 6665 writeln(x, " ", y , " ", width, " ", height); 6666 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 6667 +/ 6668 6669 registerMovement(); 6670 6671 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 6672 hsb.x = 0; 6673 hsb.y = this.height - hsb.height; 6674 hsb.width = this.width - scaleWithDpi(16); 6675 hsb.recomputeChildLayout(); 6676 6677 vsb.width = scaleWithDpi(16); // FIXME? 6678 vsb.x = this.width - vsb.width; 6679 vsb.y = 0; 6680 vsb.height = this.height - scaleWithDpi(16); 6681 vsb.recomputeChildLayout(); 6682 6683 container.x = 0; 6684 container.y = 0; 6685 container.width = this.width - vsb.width; 6686 container.height = this.height - hsb.height; 6687 container.recomputeChildLayout(); 6688 6689 scrollX_ = 0; 6690 scrollY_ = 0; 6691 6692 hsb.setPosition(0); 6693 vsb.setPosition(0); 6694 6695 int mw, mh; 6696 Widget c = container; 6697 // FIXME: hack here to handle a layout inside... 6698 if(c.children.length == 1 && cast(Layout) c.children[0]) 6699 c = c.children[0]; 6700 foreach(child; c.children) { 6701 auto w = child.x + child.width; 6702 auto h = child.y + child.height; 6703 6704 if(w > mw) mw = w; 6705 if(h > mh) mh = h; 6706 } 6707 6708 setTotalArea(mw, mh); 6709 setViewableArea(width, height); 6710 } 6711 6712 override int minHeight() { return scaleWithDpi(64); } 6713 6714 HorizontalScrollbar hsb; 6715 VerticalScrollbar vsb; 6716 ContainerWidget container; 6717 } 6718 6719 6720 version(custom_widgets) 6721 deprecated 6722 private class InternalScrollableContainerWidget : Widget { 6723 6724 ScrollableWidget sw; 6725 6726 VerticalScrollbar verticalScrollBar; 6727 HorizontalScrollbar horizontalScrollBar; 6728 6729 this(ScrollableWidget sw, Widget parent) { 6730 this.sw = sw; 6731 6732 this.tabStop = false; 6733 6734 super(parent); 6735 6736 horizontalScrollBar = new HorizontalScrollbar(this); 6737 verticalScrollBar = new VerticalScrollbar(this); 6738 6739 horizontalScrollBar.showing_ = false; 6740 verticalScrollBar.showing_ = false; 6741 6742 horizontalScrollBar.addEventListener("scrolltonextline", { 6743 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 6744 sw.horizontalScrollTo(horizontalScrollBar.position); 6745 }); 6746 horizontalScrollBar.addEventListener("scrolltopreviousline", { 6747 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 6748 sw.horizontalScrollTo(horizontalScrollBar.position); 6749 }); 6750 verticalScrollBar.addEventListener("scrolltonextline", { 6751 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 6752 sw.verticalScrollTo(verticalScrollBar.position); 6753 }); 6754 verticalScrollBar.addEventListener("scrolltopreviousline", { 6755 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 6756 sw.verticalScrollTo(verticalScrollBar.position); 6757 }); 6758 horizontalScrollBar.addEventListener("scrolltonextpage", { 6759 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 6760 sw.horizontalScrollTo(horizontalScrollBar.position); 6761 }); 6762 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 6763 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 6764 sw.horizontalScrollTo(horizontalScrollBar.position); 6765 }); 6766 verticalScrollBar.addEventListener("scrolltonextpage", { 6767 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 6768 sw.verticalScrollTo(verticalScrollBar.position); 6769 }); 6770 verticalScrollBar.addEventListener("scrolltopreviouspage", { 6771 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 6772 sw.verticalScrollTo(verticalScrollBar.position); 6773 }); 6774 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 6775 horizontalScrollBar.setPosition(event.intValue); 6776 sw.horizontalScrollTo(horizontalScrollBar.position); 6777 }); 6778 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 6779 verticalScrollBar.setPosition(event.intValue); 6780 sw.verticalScrollTo(verticalScrollBar.position); 6781 }); 6782 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 6783 horizontalScrollBar.setPosition(event.intValue); 6784 sw.horizontalScrollTo(horizontalScrollBar.position); 6785 }); 6786 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 6787 verticalScrollBar.setPosition(event.intValue); 6788 }); 6789 } 6790 6791 // this is supposed to be basically invisible... 6792 override int minWidth() { return sw.minWidth; } 6793 override int minHeight() { return sw.minHeight; } 6794 override int maxWidth() { return sw.maxWidth; } 6795 override int maxHeight() { return sw.maxHeight; } 6796 override int widthStretchiness() { return sw.widthStretchiness; } 6797 override int heightStretchiness() { return sw.heightStretchiness; } 6798 override int marginLeft() { return sw.marginLeft; } 6799 override int marginRight() { return sw.marginRight; } 6800 override int marginTop() { return sw.marginTop; } 6801 override int marginBottom() { return sw.marginBottom; } 6802 override int paddingLeft() { return sw.paddingLeft; } 6803 override int paddingRight() { return sw.paddingRight; } 6804 override int paddingTop() { return sw.paddingTop; } 6805 override int paddingBottom() { return sw.paddingBottom; } 6806 override void focus() { sw.focus(); } 6807 6808 6809 override void recomputeChildLayout() { 6810 // The stupid thing needs to calculate if a scroll bar is needed... 6811 recomputeChildLayoutHelper(); 6812 // then running it again will position things correctly if the bar is NOT needed 6813 recomputeChildLayoutHelper(); 6814 6815 // this sucks but meh it barely works 6816 } 6817 6818 private void recomputeChildLayoutHelper() { 6819 if(sw is null) return; 6820 6821 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 6822 if(horizontalScrollBar && verticalScrollBar) { 6823 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 6824 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 6825 horizontalScrollBar.x = 0; 6826 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 6827 6828 verticalScrollBar.width = verticalScrollBar.minWidth(); 6829 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 6830 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 6831 verticalScrollBar.y = 0 + 2; 6832 6833 sw.x = 0; 6834 sw.y = 0; 6835 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 6836 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 6837 6838 if(sw.contentWidth_ <= this.width) 6839 sw.scrollOrigin_.x = 0; 6840 if(sw.contentHeight_ <= this.height) 6841 sw.scrollOrigin_.y = 0; 6842 6843 horizontalScrollBar.recomputeChildLayout(); 6844 verticalScrollBar.recomputeChildLayout(); 6845 sw.recomputeChildLayout(); 6846 } 6847 6848 if(sw.contentWidth_ <= this.width) 6849 sw.scrollOrigin_.x = 0; 6850 if(sw.contentHeight_ <= this.height) 6851 sw.scrollOrigin_.y = 0; 6852 6853 if(sw.showingHorizontalScroll()) 6854 horizontalScrollBar.showing(true, false); 6855 else 6856 horizontalScrollBar.showing(false, false); 6857 if(sw.showingVerticalScroll()) 6858 verticalScrollBar.showing(true, false); 6859 else 6860 verticalScrollBar.showing(false, false); 6861 6862 verticalScrollBar.setViewableArea(sw.viewportHeight()); 6863 verticalScrollBar.setMax(sw.contentHeight); 6864 verticalScrollBar.setPosition(sw.scrollOrigin.y); 6865 6866 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 6867 horizontalScrollBar.setMax(sw.contentWidth); 6868 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 6869 } 6870 } 6871 6872 /* 6873 class ScrollableClientWidget : Widget { 6874 this(Widget parent) { 6875 super(parent); 6876 } 6877 override void paint(WidgetPainter p) { 6878 parent.paint(p); 6879 } 6880 } 6881 */ 6882 6883 /++ 6884 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. 6885 +/ 6886 abstract class Slider : Widget { 6887 this(int min, int max, int step, Widget parent) { 6888 min_ = min; 6889 max_ = max; 6890 step_ = step; 6891 page_ = step; 6892 super(parent); 6893 } 6894 6895 private int min_; 6896 private int max_; 6897 private int step_; 6898 private int position_; 6899 private int page_; 6900 6901 // selection start and selection end 6902 // tics 6903 // tooltip? 6904 // some way to see and just type the value 6905 // win32 buddy controls are labels 6906 6907 /// 6908 void setMin(int a) { 6909 min_ = a; 6910 version(custom_widgets) 6911 redraw(); 6912 version(win32_widgets) 6913 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 6914 } 6915 /// 6916 int min() { 6917 return min_; 6918 } 6919 /// 6920 void setMax(int a) { 6921 max_ = a; 6922 version(custom_widgets) 6923 redraw(); 6924 version(win32_widgets) 6925 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 6926 } 6927 /// 6928 int max() { 6929 return max_; 6930 } 6931 /// 6932 void setPosition(int a) { 6933 if(a > max) 6934 a = max; 6935 if(a < min) 6936 a = min; 6937 position_ = a; 6938 version(custom_widgets) 6939 setPositionCustom(a); 6940 6941 version(win32_widgets) 6942 setPositionWindows(a); 6943 } 6944 version(win32_widgets) { 6945 protected abstract void setPositionWindows(int a); 6946 } 6947 6948 protected abstract int win32direction(); 6949 6950 /++ 6951 Alias for [position] for better compatibility with generic code. 6952 6953 History: 6954 Added October 5, 2021 6955 +/ 6956 @property int value() { 6957 return position; 6958 } 6959 6960 /// 6961 int position() { 6962 return position_; 6963 } 6964 /// 6965 void setStep(int a) { 6966 step_ = a; 6967 version(win32_widgets) 6968 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 6969 } 6970 /// 6971 int step() { 6972 return step_; 6973 } 6974 /// 6975 void setPageSize(int a) { 6976 page_ = a; 6977 version(win32_widgets) 6978 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 6979 } 6980 /// 6981 int pageSize() { 6982 return page_; 6983 } 6984 6985 private void notify() { 6986 auto event = new ChangeEvent!int(this, &this.position); 6987 event.dispatch(); 6988 } 6989 6990 version(win32_widgets) 6991 void win32Setup(int style) { 6992 createWin32Window(this, TRACKBAR_CLASS, "", 6993 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 6994 6995 // the trackbar sends the same messages as scroll, which 6996 // our other layer sends as these... just gonna translate 6997 // here 6998 this.addDirectEventListener("scrolltoposition", (Event event) { 6999 event.stopPropagation(); 7000 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 7001 notify(); 7002 }); 7003 this.addDirectEventListener("scrolltonextline", (Event event) { 7004 event.stopPropagation(); 7005 this.setPosition(this.position + this.step_ * this.win32direction); 7006 notify(); 7007 }); 7008 this.addDirectEventListener("scrolltopreviousline", (Event event) { 7009 event.stopPropagation(); 7010 this.setPosition(this.position - this.step_ * this.win32direction); 7011 notify(); 7012 }); 7013 this.addDirectEventListener("scrolltonextpage", (Event event) { 7014 event.stopPropagation(); 7015 this.setPosition(this.position + this.page_ * this.win32direction); 7016 notify(); 7017 }); 7018 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 7019 event.stopPropagation(); 7020 this.setPosition(this.position - this.page_ * this.win32direction); 7021 notify(); 7022 }); 7023 7024 setMin(min_); 7025 setMax(max_); 7026 setStep(step_); 7027 setPageSize(page_); 7028 } 7029 7030 version(custom_widgets) { 7031 protected MouseTrackingWidget thumb; 7032 7033 protected abstract void setPositionCustom(int a); 7034 7035 override void defaultEventHandler_keydown(KeyDownEvent event) { 7036 switch(event.key) { 7037 case Key.Up: 7038 case Key.Right: 7039 setPosition(position() - step() * win32direction); 7040 changed(); 7041 break; 7042 case Key.Down: 7043 case Key.Left: 7044 setPosition(position() + step() * win32direction); 7045 changed(); 7046 break; 7047 case Key.Home: 7048 setPosition(win32direction > 0 ? min() : max()); 7049 changed(); 7050 break; 7051 case Key.End: 7052 setPosition(win32direction > 0 ? max() : min()); 7053 changed(); 7054 break; 7055 case Key.PageUp: 7056 setPosition(position() - pageSize() * win32direction); 7057 changed(); 7058 break; 7059 case Key.PageDown: 7060 setPosition(position() + pageSize() * win32direction); 7061 changed(); 7062 break; 7063 default: 7064 } 7065 super.defaultEventHandler_keydown(event); 7066 } 7067 7068 protected void changed() { 7069 auto ev = new ChangeEvent!int(this, &position); 7070 ev.dispatch(); 7071 } 7072 } 7073 } 7074 7075 /++ 7076 7077 +/ 7078 class VerticalSlider : Slider { 7079 this(int min, int max, int step, Widget parent) { 7080 version(custom_widgets) 7081 initialize(); 7082 7083 super(min, max, step, parent); 7084 7085 version(win32_widgets) 7086 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 7087 } 7088 7089 protected override int win32direction() { 7090 return -1; 7091 } 7092 7093 version(win32_widgets) 7094 protected override void setPositionWindows(int a) { 7095 // the windows thing makes the top 0 and i don't like that. 7096 SendMessage(hwnd, TBM_SETPOS, true, max - a); 7097 } 7098 7099 version(custom_widgets) 7100 private void initialize() { 7101 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 7102 7103 thumb.tabStop = false; 7104 7105 thumb.thumbWidth = width; 7106 thumb.thumbHeight = scaleWithDpi(16); 7107 7108 thumb.addEventListener(EventType.change, () { 7109 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 7110 sx = max - sx; 7111 //informProgramThatUserChangedPosition(sx); 7112 7113 position_ = sx; 7114 7115 changed(); 7116 }); 7117 } 7118 7119 version(custom_widgets) 7120 override void recomputeChildLayout() { 7121 thumb.thumbWidth = this.width; 7122 super.recomputeChildLayout(); 7123 setPositionCustom(position_); 7124 } 7125 7126 version(custom_widgets) 7127 protected override void setPositionCustom(int a) { 7128 if(max()) 7129 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 7130 redraw(); 7131 } 7132 } 7133 7134 /++ 7135 7136 +/ 7137 class HorizontalSlider : Slider { 7138 this(int min, int max, int step, Widget parent) { 7139 version(custom_widgets) 7140 initialize(); 7141 7142 super(min, max, step, parent); 7143 7144 version(win32_widgets) 7145 win32Setup(TBS_HORZ); 7146 } 7147 7148 version(win32_widgets) 7149 protected override void setPositionWindows(int a) { 7150 SendMessage(hwnd, TBM_SETPOS, true, a); 7151 } 7152 7153 protected override int win32direction() { 7154 return 1; 7155 } 7156 7157 version(custom_widgets) 7158 private void initialize() { 7159 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 7160 7161 thumb.tabStop = false; 7162 7163 thumb.thumbWidth = scaleWithDpi(16); 7164 thumb.thumbHeight = height; 7165 7166 thumb.addEventListener(EventType.change, () { 7167 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 7168 //informProgramThatUserChangedPosition(sx); 7169 7170 position_ = sx; 7171 7172 changed(); 7173 }); 7174 } 7175 7176 version(custom_widgets) 7177 override void recomputeChildLayout() { 7178 thumb.thumbHeight = this.height; 7179 super.recomputeChildLayout(); 7180 setPositionCustom(position_); 7181 } 7182 7183 version(custom_widgets) 7184 protected override void setPositionCustom(int a) { 7185 if(max()) 7186 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 7187 redraw(); 7188 } 7189 } 7190 7191 7192 /// 7193 abstract class ScrollbarBase : Widget { 7194 /// 7195 this(Widget parent) { 7196 super(parent); 7197 tabStop = false; 7198 step_ = scaleWithDpi(16); 7199 } 7200 7201 private int viewableArea_; 7202 private int max_; 7203 private int step_;// = 16; 7204 private int position_; 7205 7206 /// 7207 bool atEnd() { 7208 return position_ + viewableArea_ >= max_; 7209 } 7210 7211 /// 7212 bool atStart() { 7213 return position_ == 0; 7214 } 7215 7216 /// 7217 void setViewableArea(int a) { 7218 viewableArea_ = a; 7219 version(custom_widgets) 7220 redraw(); 7221 } 7222 /// 7223 void setMax(int a) { 7224 max_ = a; 7225 version(custom_widgets) 7226 redraw(); 7227 } 7228 /// 7229 int max() { 7230 return max_; 7231 } 7232 /// 7233 void setPosition(int a) { 7234 auto logicalMax = max_ - viewableArea_; 7235 if(a == int.max) 7236 a = logicalMax; 7237 7238 if(a > logicalMax) 7239 a = logicalMax; 7240 if(a < 0) 7241 a = 0; 7242 7243 position_ = a; 7244 7245 version(custom_widgets) 7246 redraw(); 7247 } 7248 /// 7249 int position() { 7250 return position_; 7251 } 7252 /// 7253 void setStep(int a) { 7254 step_ = a; 7255 } 7256 /// 7257 int step() { 7258 return step_; 7259 } 7260 7261 // FIXME: remove this.... maybe 7262 /+ 7263 protected void informProgramThatUserChangedPosition(int n) { 7264 position_ = n; 7265 auto evt = new Event(EventType.change, this); 7266 evt.intValue = n; 7267 evt.dispatch(); 7268 } 7269 +/ 7270 7271 version(custom_widgets) { 7272 enum MIN_THUMB_SIZE = 8; 7273 7274 abstract protected int getBarDim(); 7275 int thumbSize() { 7276 if(viewableArea_ >= max_ || max_ == 0) 7277 return getBarDim(); 7278 7279 int res = viewableArea_ * getBarDim() / max_; 7280 7281 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 7282 res = scaleWithDpi(MIN_THUMB_SIZE); 7283 7284 return res; 7285 } 7286 7287 int thumbPosition() { 7288 /* 7289 viewableArea_ is the viewport height/width 7290 position_ is where we are 7291 */ 7292 //if(position_ + viewableArea_ >= max_) 7293 //return getBarDim - thumbSize; 7294 7295 auto maximumPossibleValue = getBarDim() - thumbSize; 7296 auto maximiumLogicalValue = max_ - viewableArea_; 7297 7298 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 7299 7300 return p; 7301 } 7302 } 7303 } 7304 7305 //public import mgt; 7306 7307 /++ 7308 A mouse tracking widget is one that follows the mouse when dragged inside it. 7309 7310 Concrete subclasses may include a scrollbar thumb and a volume control. 7311 +/ 7312 //version(custom_widgets) 7313 class MouseTrackingWidget : Widget { 7314 7315 /// 7316 int positionX() { return positionX_; } 7317 /// 7318 int positionY() { return positionY_; } 7319 7320 /// 7321 void positionX(int p) { positionX_ = p; } 7322 /// 7323 void positionY(int p) { positionY_ = p; } 7324 7325 private int positionX_; 7326 private int positionY_; 7327 7328 /// 7329 enum Orientation { 7330 horizontal, /// 7331 vertical, /// 7332 twoDimensional, /// 7333 } 7334 7335 private int thumbWidth_; 7336 private int thumbHeight_; 7337 7338 /// 7339 int thumbWidth() { return thumbWidth_; } 7340 /// 7341 int thumbHeight() { return thumbHeight_; } 7342 /// 7343 int thumbWidth(int a) { return thumbWidth_ = a; } 7344 /// 7345 int thumbHeight(int a) { return thumbHeight_ = a; } 7346 7347 private bool dragging; 7348 private bool hovering; 7349 private int startMouseX, startMouseY; 7350 7351 /// 7352 this(Orientation orientation, Widget parent) { 7353 super(parent); 7354 7355 //assert(parentWindow !is null); 7356 7357 addEventListener((MouseDownEvent event) { 7358 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7359 dragging = true; 7360 startMouseX = event.clientX - positionX; 7361 startMouseY = event.clientY - positionY; 7362 parentWindow.captureMouse(this); 7363 } else { 7364 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7365 positionX = event.clientX - thumbWidth / 2; 7366 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7367 positionY = event.clientY - thumbHeight / 2; 7368 7369 if(positionX + thumbWidth > this.width) 7370 positionX = this.width - thumbWidth; 7371 if(positionY + thumbHeight > this.height) 7372 positionY = this.height - thumbHeight; 7373 7374 if(positionX < 0) 7375 positionX = 0; 7376 if(positionY < 0) 7377 positionY = 0; 7378 7379 7380 // this.emit!(ChangeEvent!void)(); 7381 auto evt = new Event(EventType.change, this); 7382 evt.sendDirectly(); 7383 7384 redraw(); 7385 7386 } 7387 }); 7388 7389 addEventListener(EventType.mouseup, (Event event) { 7390 dragging = false; 7391 parentWindow.releaseMouseCapture(); 7392 }); 7393 7394 addEventListener(EventType.mouseout, (Event event) { 7395 if(!hovering) 7396 return; 7397 hovering = false; 7398 redraw(); 7399 }); 7400 7401 int lpx, lpy; 7402 7403 addEventListener((MouseMoveEvent event) { 7404 auto oh = hovering; 7405 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7406 hovering = true; 7407 } else { 7408 hovering = false; 7409 } 7410 if(!dragging) { 7411 if(hovering != oh) 7412 redraw(); 7413 return; 7414 } 7415 7416 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7417 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 7418 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7419 positionY = event.clientY - startMouseY; 7420 7421 if(positionX + thumbWidth > this.width) 7422 positionX = this.width - thumbWidth; 7423 if(positionY + thumbHeight > this.height) 7424 positionY = this.height - thumbHeight; 7425 7426 if(positionX < 0) 7427 positionX = 0; 7428 if(positionY < 0) 7429 positionY = 0; 7430 7431 if(positionX != lpx || positionY != lpy) { 7432 lpx = positionX; 7433 lpy = positionY; 7434 7435 auto evt = new Event(EventType.change, this); 7436 evt.sendDirectly(); 7437 } 7438 7439 redraw(); 7440 }); 7441 } 7442 7443 version(custom_widgets) 7444 override void paint(WidgetPainter painter) { 7445 auto cs = getComputedStyle(); 7446 auto c = darken(cs.windowBackgroundColor, 0.2); 7447 painter.outlineColor = c; 7448 painter.fillColor = c; 7449 painter.drawRectangle(Point(0, 0), this.width, this.height); 7450 7451 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 7452 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 7453 } 7454 } 7455 7456 //version(custom_widgets) 7457 //private 7458 class HorizontalScrollbar : ScrollbarBase { 7459 7460 version(custom_widgets) { 7461 private MouseTrackingWidget thumb; 7462 7463 override int getBarDim() { 7464 return thumb.width; 7465 } 7466 } 7467 7468 override void setViewableArea(int a) { 7469 super.setViewableArea(a); 7470 7471 version(win32_widgets) { 7472 SCROLLINFO info; 7473 info.cbSize = info.sizeof; 7474 info.nPage = a + 1; 7475 info.fMask = SIF_PAGE; 7476 SetScrollInfo(hwnd, SB_CTL, &info, true); 7477 } else version(custom_widgets) { 7478 thumb.positionX = thumbPosition; 7479 thumb.thumbWidth = thumbSize; 7480 thumb.redraw(); 7481 } else static assert(0); 7482 7483 } 7484 7485 override void setMax(int a) { 7486 super.setMax(a); 7487 version(win32_widgets) { 7488 SCROLLINFO info; 7489 info.cbSize = info.sizeof; 7490 info.nMin = 0; 7491 info.nMax = max; 7492 info.fMask = SIF_RANGE; 7493 SetScrollInfo(hwnd, SB_CTL, &info, true); 7494 } else version(custom_widgets) { 7495 thumb.positionX = thumbPosition; 7496 thumb.thumbWidth = thumbSize; 7497 thumb.redraw(); 7498 } 7499 } 7500 7501 override void setPosition(int a) { 7502 super.setPosition(a); 7503 version(win32_widgets) { 7504 SCROLLINFO info; 7505 info.cbSize = info.sizeof; 7506 info.fMask = SIF_POS; 7507 info.nPos = position; 7508 SetScrollInfo(hwnd, SB_CTL, &info, true); 7509 } else version(custom_widgets) { 7510 thumb.positionX = thumbPosition(); 7511 thumb.thumbWidth = thumbSize; 7512 thumb.redraw(); 7513 } else static assert(0); 7514 } 7515 7516 this(Widget parent) { 7517 super(parent); 7518 7519 version(win32_widgets) { 7520 createWin32Window(this, "Scrollbar"w, "", 7521 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 7522 } else version(custom_widgets) { 7523 auto vl = new HorizontalLayout(this); 7524 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 7525 leftButton.setClickRepeat(scrollClickRepeatInterval); 7526 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 7527 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 7528 rightButton.setClickRepeat(scrollClickRepeatInterval); 7529 7530 leftButton.tabStop = false; 7531 rightButton.tabStop = false; 7532 thumb.tabStop = false; 7533 7534 leftButton.addEventListener(EventType.triggered, () { 7535 this.emitCommand!"scrolltopreviousline"(); 7536 //informProgramThatUserChangedPosition(position - step()); 7537 }); 7538 rightButton.addEventListener(EventType.triggered, () { 7539 this.emitCommand!"scrolltonextline"(); 7540 //informProgramThatUserChangedPosition(position + step()); 7541 }); 7542 7543 thumb.thumbWidth = this.minWidth; 7544 thumb.thumbHeight = scaleWithDpi(16); 7545 7546 thumb.addEventListener(EventType.change, () { 7547 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 7548 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 7549 7550 //informProgramThatUserChangedPosition(sx); 7551 7552 auto ev = new ScrollToPositionEvent(this, sx); 7553 ev.dispatch(); 7554 }); 7555 } 7556 } 7557 7558 override int minHeight() { return scaleWithDpi(16); } 7559 override int maxHeight() { return scaleWithDpi(16); } 7560 override int minWidth() { return scaleWithDpi(48); } 7561 } 7562 7563 final class ScrollToPositionEvent : Event { 7564 enum EventString = "scrolltoposition"; 7565 7566 this(Widget target, int value) { 7567 this.value = value; 7568 super(EventString, target); 7569 } 7570 7571 immutable int value; 7572 7573 override @property int intValue() { 7574 return value; 7575 } 7576 } 7577 7578 //version(custom_widgets) 7579 //private 7580 class VerticalScrollbar : ScrollbarBase { 7581 7582 version(custom_widgets) { 7583 override int getBarDim() { 7584 return thumb.height; 7585 } 7586 7587 private MouseTrackingWidget thumb; 7588 } 7589 7590 override void setViewableArea(int a) { 7591 super.setViewableArea(a); 7592 7593 version(win32_widgets) { 7594 SCROLLINFO info; 7595 info.cbSize = info.sizeof; 7596 info.nPage = a + 1; 7597 info.fMask = SIF_PAGE; 7598 SetScrollInfo(hwnd, SB_CTL, &info, true); 7599 } else version(custom_widgets) { 7600 thumb.positionY = thumbPosition; 7601 thumb.thumbHeight = thumbSize; 7602 thumb.redraw(); 7603 } else static assert(0); 7604 7605 } 7606 7607 override void setMax(int a) { 7608 super.setMax(a); 7609 version(win32_widgets) { 7610 SCROLLINFO info; 7611 info.cbSize = info.sizeof; 7612 info.nMin = 0; 7613 info.nMax = max; 7614 info.fMask = SIF_RANGE; 7615 SetScrollInfo(hwnd, SB_CTL, &info, true); 7616 } else version(custom_widgets) { 7617 thumb.positionY = thumbPosition; 7618 thumb.thumbHeight = thumbSize; 7619 thumb.redraw(); 7620 } 7621 } 7622 7623 override void setPosition(int a) { 7624 super.setPosition(a); 7625 version(win32_widgets) { 7626 SCROLLINFO info; 7627 info.cbSize = info.sizeof; 7628 info.fMask = SIF_POS; 7629 info.nPos = position; 7630 SetScrollInfo(hwnd, SB_CTL, &info, true); 7631 } else version(custom_widgets) { 7632 thumb.positionY = thumbPosition; 7633 thumb.thumbHeight = thumbSize; 7634 thumb.redraw(); 7635 } else static assert(0); 7636 } 7637 7638 this(Widget parent) { 7639 super(parent); 7640 7641 version(win32_widgets) { 7642 createWin32Window(this, "Scrollbar"w, "", 7643 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 7644 } else version(custom_widgets) { 7645 auto vl = new VerticalLayout(this); 7646 auto upButton = new ArrowButton(ArrowDirection.up, vl); 7647 upButton.setClickRepeat(scrollClickRepeatInterval); 7648 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 7649 auto downButton = new ArrowButton(ArrowDirection.down, vl); 7650 downButton.setClickRepeat(scrollClickRepeatInterval); 7651 7652 upButton.addEventListener(EventType.triggered, () { 7653 this.emitCommand!"scrolltopreviousline"(); 7654 //informProgramThatUserChangedPosition(position - step()); 7655 }); 7656 downButton.addEventListener(EventType.triggered, () { 7657 this.emitCommand!"scrolltonextline"(); 7658 //informProgramThatUserChangedPosition(position + step()); 7659 }); 7660 7661 thumb.thumbWidth = this.minWidth; 7662 thumb.thumbHeight = scaleWithDpi(16); 7663 7664 thumb.addEventListener(EventType.change, () { 7665 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 7666 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 7667 7668 auto ev = new ScrollToPositionEvent(this, sy); 7669 ev.dispatch(); 7670 7671 //informProgramThatUserChangedPosition(sy); 7672 }); 7673 7674 upButton.tabStop = false; 7675 downButton.tabStop = false; 7676 thumb.tabStop = false; 7677 } 7678 } 7679 7680 override int minWidth() { return scaleWithDpi(16); } 7681 override int maxWidth() { return scaleWithDpi(16); } 7682 override int minHeight() { return scaleWithDpi(48); } 7683 } 7684 7685 7686 /++ 7687 EXPERIMENTAL 7688 7689 A widget specialized for being a container for other widgets. 7690 7691 History: 7692 Added May 29, 2021. Not stabilized at this time. 7693 +/ 7694 class WidgetContainer : Widget { 7695 this(Widget parent) { 7696 tabStop = false; 7697 super(parent); 7698 } 7699 7700 override int maxHeight() { 7701 if(this.children.length == 1) { 7702 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 7703 } else { 7704 return int.max; 7705 } 7706 } 7707 7708 override int maxWidth() { 7709 if(this.children.length == 1) { 7710 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 7711 } else { 7712 return int.max; 7713 } 7714 } 7715 7716 /+ 7717 7718 override int minHeight() { 7719 int largest = 0; 7720 int margins = 0; 7721 int lastMargin = 0; 7722 foreach(child; children) { 7723 auto mh = child.minHeight(); 7724 if(mh > largest) 7725 largest = mh; 7726 margins += mymax(lastMargin, child.marginTop()); 7727 lastMargin = child.marginBottom(); 7728 } 7729 return largest + margins; 7730 } 7731 7732 override int maxHeight() { 7733 int largest = 0; 7734 int margins = 0; 7735 int lastMargin = 0; 7736 foreach(child; children) { 7737 auto mh = child.maxHeight(); 7738 if(mh == int.max) 7739 return int.max; 7740 if(mh > largest) 7741 largest = mh; 7742 margins += mymax(lastMargin, child.marginTop()); 7743 lastMargin = child.marginBottom(); 7744 } 7745 return largest + margins; 7746 } 7747 7748 override int minWidth() { 7749 int min; 7750 foreach(child; children) { 7751 auto cm = child.minWidth; 7752 if(cm > min) 7753 min = cm; 7754 } 7755 return min + paddingLeft + paddingRight; 7756 } 7757 7758 override int minHeight() { 7759 int min; 7760 foreach(child; children) { 7761 auto cm = child.minHeight; 7762 if(cm > min) 7763 min = cm; 7764 } 7765 return min + paddingTop + paddingBottom; 7766 } 7767 7768 override int maxHeight() { 7769 int largest = 0; 7770 int margins = 0; 7771 int lastMargin = 0; 7772 foreach(child; children) { 7773 auto mh = child.maxHeight(); 7774 if(mh == int.max) 7775 return int.max; 7776 if(mh > largest) 7777 largest = mh; 7778 margins += mymax(lastMargin, child.marginTop()); 7779 lastMargin = child.marginBottom(); 7780 } 7781 return largest + margins; 7782 } 7783 7784 override int heightStretchiness() { 7785 int max; 7786 foreach(child; children) { 7787 auto c = child.heightStretchiness; 7788 if(c > max) 7789 max = c; 7790 } 7791 return max; 7792 } 7793 7794 override int marginTop() { 7795 if(this.children.length) 7796 return this.children[0].marginTop; 7797 return 0; 7798 } 7799 +/ 7800 } 7801 7802 /// 7803 abstract class Layout : Widget { 7804 this(Widget parent) { 7805 tabStop = false; 7806 super(parent); 7807 } 7808 } 7809 7810 /++ 7811 Makes all children minimum width and height, placing them down 7812 left to right, top to bottom. 7813 7814 Useful if you want to make a list of buttons that automatically 7815 wrap to a new line when necessary. 7816 +/ 7817 class InlineBlockLayout : Layout { 7818 /// 7819 this(Widget parent) { super(parent); } 7820 7821 override void recomputeChildLayout() { 7822 registerMovement(); 7823 7824 int x = this.paddingLeft, y = this.paddingTop; 7825 7826 int lineHeight; 7827 int previousMargin = 0; 7828 int previousMarginBottom = 0; 7829 7830 foreach(child; children) { 7831 if(child.hidden) 7832 continue; 7833 if(cast(FixedPosition) child) { 7834 child.recomputeChildLayout(); 7835 continue; 7836 } 7837 child.width = child.flexBasisWidth(); 7838 if(child.width == 0) 7839 child.width = child.minWidth(); 7840 if(child.width == 0) 7841 child.width = 32; 7842 7843 child.height = child.flexBasisHeight(); 7844 if(child.height == 0) 7845 child.height = child.minHeight(); 7846 if(child.height == 0) 7847 child.height = 32; 7848 7849 if(x + child.width + paddingRight > this.width) { 7850 x = this.paddingLeft; 7851 y += lineHeight; 7852 lineHeight = 0; 7853 previousMargin = 0; 7854 previousMarginBottom = 0; 7855 } 7856 7857 auto margin = child.marginLeft; 7858 if(previousMargin > margin) 7859 margin = previousMargin; 7860 7861 x += margin; 7862 7863 child.x = x; 7864 child.y = y; 7865 7866 int marginTopApplied; 7867 if(child.marginTop > previousMarginBottom) { 7868 child.y += child.marginTop; 7869 marginTopApplied = child.marginTop; 7870 } 7871 7872 x += child.width; 7873 previousMargin = child.marginRight; 7874 7875 if(child.marginBottom > previousMarginBottom) 7876 previousMarginBottom = child.marginBottom; 7877 7878 auto h = child.height + previousMarginBottom + marginTopApplied; 7879 if(h > lineHeight) 7880 lineHeight = h; 7881 7882 child.recomputeChildLayout(); 7883 } 7884 7885 } 7886 7887 override int minWidth() { 7888 int min; 7889 foreach(child; children) { 7890 auto cm = child.minWidth; 7891 if(cm > min) 7892 min = cm; 7893 } 7894 return min + paddingLeft + paddingRight; 7895 } 7896 7897 override int minHeight() { 7898 int min; 7899 foreach(child; children) { 7900 auto cm = child.minHeight; 7901 if(cm > min) 7902 min = cm; 7903 } 7904 return min + paddingTop + paddingBottom; 7905 } 7906 } 7907 7908 /++ 7909 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 7910 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 7911 the [TabWidget] will automatically change pages of child widgets. 7912 7913 This allows you to react to it however you see fit rather than having to 7914 be tied to just the new sets of child widgets. 7915 7916 It sends the message in the form of `this.emitCommand!"changetab"();`. 7917 7918 History: 7919 Added December 24, 2021 (dub v10.5) 7920 +/ 7921 class TabMessageWidget : Widget { 7922 7923 protected void tabIndexClicked(int item) { 7924 this.emitCommand!"changetab"(); 7925 } 7926 7927 /++ 7928 Adds the a new tab to the control with the given title. 7929 7930 Returns: 7931 The index of the newly added tab. You will need to know 7932 this index to refer to it later and to know which tab to 7933 change to when you get a changetab message. 7934 +/ 7935 int addTab(string title, int pos = int.max) { 7936 version(win32_widgets) { 7937 TCITEM item; 7938 item.mask = TCIF_TEXT; 7939 WCharzBuffer buf = WCharzBuffer(title); 7940 item.pszText = buf.ptr; 7941 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 7942 } else version(custom_widgets) { 7943 if(pos >= tabs.length) { 7944 tabs ~= title; 7945 redraw(); 7946 return cast(int) tabs.length - 1; 7947 } else if(pos <= 0) { 7948 tabs = title ~ tabs; 7949 redraw(); 7950 return 0; 7951 } else { 7952 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 7953 redraw(); 7954 return pos; 7955 } 7956 } 7957 } 7958 7959 override void addChild(Widget child, int pos = int.max) { 7960 if(container) 7961 container.addChild(child, pos); 7962 else 7963 super.addChild(child, pos); 7964 } 7965 7966 protected Widget makeContainer() { 7967 return new Widget(this); 7968 } 7969 7970 private Widget container; 7971 7972 override void recomputeChildLayout() { 7973 version(win32_widgets) { 7974 this.registerMovement(); 7975 7976 RECT rect; 7977 GetWindowRect(hwnd, &rect); 7978 7979 auto left = rect.left; 7980 auto top = rect.top; 7981 7982 TabCtrl_AdjustRect(hwnd, false, &rect); 7983 foreach(child; children) { 7984 if(!child.showing) continue; 7985 child.x = rect.left - left; 7986 child.y = rect.top - top; 7987 child.width = rect.right - rect.left; 7988 child.height = rect.bottom - rect.top; 7989 child.recomputeChildLayout(); 7990 } 7991 } else version(custom_widgets) { 7992 this.registerMovement(); 7993 foreach(child; children) { 7994 if(!child.showing) continue; 7995 child.x = 2; 7996 child.y = tabBarHeight + 2; // for the border 7997 child.width = width - 4; // for the border 7998 child.height = height - tabBarHeight - 2 - 2; // for the border 7999 child.recomputeChildLayout(); 8000 } 8001 } else static assert(0); 8002 } 8003 8004 this(Widget parent) { 8005 super(parent); 8006 8007 tabStop = false; 8008 8009 version(win32_widgets) { 8010 createWin32Window(this, WC_TABCONTROL, "", 0); 8011 } else version(custom_widgets) { 8012 addEventListener((ClickEvent event) { 8013 if(event.target !is this) 8014 return; 8015 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 8016 auto t = (event.clientX / tabWidth); 8017 if(t >= 0 && t < tabs.length) { 8018 currentTab_ = t; 8019 tabIndexClicked(t); 8020 redraw(); 8021 } 8022 } 8023 }); 8024 } else static assert(0); 8025 8026 this.container = makeContainer(); 8027 } 8028 8029 override int marginTop() { return 4; } 8030 override int paddingBottom() { return 4; } 8031 8032 override int minHeight() { 8033 int max = 0; 8034 foreach(child; children) 8035 max = mymax(child.minHeight, max); 8036 8037 8038 version(win32_widgets) { 8039 RECT rect; 8040 rect.right = this.width; 8041 rect.bottom = max; 8042 TabCtrl_AdjustRect(hwnd, true, &rect); 8043 8044 max = rect.bottom; 8045 } else { 8046 max += defaultLineHeight + 4; 8047 } 8048 8049 8050 return max; 8051 } 8052 8053 version(win32_widgets) 8054 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 8055 switch(code) { 8056 case TCN_SELCHANGE: 8057 auto sel = TabCtrl_GetCurSel(hwnd); 8058 tabIndexClicked(sel); 8059 break; 8060 default: 8061 } 8062 return 0; 8063 } 8064 8065 version(custom_widgets) { 8066 private int currentTab_; 8067 private int tabBarHeight() { return defaultLineHeight; } 8068 int tabWidth() { return scaleWithDpi(80); } 8069 8070 string[] tabs; 8071 } 8072 8073 version(win32_widgets) 8074 override void paint(WidgetPainter painter) {} 8075 8076 version(custom_widgets) 8077 override void paint(WidgetPainter painter) { 8078 auto cs = getComputedStyle(); 8079 8080 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 8081 8082 int posX = 0; 8083 foreach(idx, title; tabs) { 8084 auto isCurrent = idx == getCurrentTab(); 8085 8086 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 8087 8088 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 8089 painter.outlineColor = cs.foregroundColor; 8090 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 8091 8092 if(isCurrent) { 8093 painter.outlineColor = cs.windowBackgroundColor; 8094 painter.fillColor = Color.transparent; 8095 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 8096 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 8097 8098 painter.outlineColor = Color.white; 8099 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 8100 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 8101 painter.outlineColor = cs.activeTabColor; 8102 painter.drawPixel(Point(posX, tabBarHeight - 1)); 8103 } 8104 8105 posX += tabWidth - 2; 8106 } 8107 } 8108 8109 /// 8110 @scriptable 8111 void setCurrentTab(int item) { 8112 version(win32_widgets) 8113 TabCtrl_SetCurSel(hwnd, item); 8114 else version(custom_widgets) 8115 currentTab_ = item; 8116 else static assert(0); 8117 8118 tabIndexClicked(item); 8119 } 8120 8121 /// 8122 @scriptable 8123 int getCurrentTab() { 8124 version(win32_widgets) 8125 return TabCtrl_GetCurSel(hwnd); 8126 else version(custom_widgets) 8127 return currentTab_; // FIXME 8128 else static assert(0); 8129 } 8130 8131 /// 8132 @scriptable 8133 void removeTab(int item) { 8134 if(item && item == getCurrentTab()) 8135 setCurrentTab(item - 1); 8136 8137 version(win32_widgets) { 8138 TabCtrl_DeleteItem(hwnd, item); 8139 } 8140 8141 for(int a = item; a < children.length - 1; a++) 8142 this._children[a] = this._children[a + 1]; 8143 this._children = this._children[0 .. $-1]; 8144 } 8145 8146 } 8147 8148 8149 /++ 8150 A tab widget is a set of clickable tab buttons followed by a content area. 8151 8152 8153 Tabs can change existing content or can be new pages. 8154 8155 When the user picks a different tab, a `change` message is generated. 8156 +/ 8157 class TabWidget : TabMessageWidget { 8158 this(Widget parent) { 8159 super(parent); 8160 } 8161 8162 override protected Widget makeContainer() { 8163 return null; 8164 } 8165 8166 override void addChild(Widget child, int pos = int.max) { 8167 if(auto twp = cast(TabWidgetPage) child) { 8168 Widget.addChild(child, pos); 8169 if(pos == int.max) 8170 pos = cast(int) this.children.length - 1; 8171 8172 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 8173 8174 if(pos != getCurrentTab) { 8175 child.showing = false; 8176 } 8177 } else { 8178 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 8179 } 8180 } 8181 8182 // FIXME: add tab icons at some point, Windows supports them 8183 /++ 8184 Adds a page and its associated tab with the given label to the widget. 8185 8186 Returns: 8187 The added page object, to which you can add other widgets. 8188 +/ 8189 @scriptable 8190 TabWidgetPage addPage(string title) { 8191 return new TabWidgetPage(title, this); 8192 } 8193 8194 /++ 8195 Gets the page at the given tab index, or `null` if the index is bad. 8196 8197 History: 8198 Added December 24, 2021. 8199 +/ 8200 TabWidgetPage getPage(int index) { 8201 if(index < this.children.length) 8202 return null; 8203 return cast(TabWidgetPage) this.children[index]; 8204 } 8205 8206 /++ 8207 While you can still use the addTab from the parent class, 8208 *strongly* recommend you use [addPage] insteaad. 8209 8210 History: 8211 Added December 24, 2021 to fulful the interface 8212 requirement that came from adding [TabMessageWidget]. 8213 8214 You should not use it though since the [addPage] function 8215 is much easier to use here. 8216 +/ 8217 override int addTab(string title, int pos = int.max) { 8218 auto p = addPage(title); 8219 foreach(idx, child; this.children) 8220 if(child is p) 8221 return cast(int) idx; 8222 return -1; 8223 } 8224 8225 protected override void tabIndexClicked(int item) { 8226 foreach(idx, child; children) { 8227 child.showing(false, false); // batch the recalculates for the end 8228 } 8229 8230 foreach(idx, child; children) { 8231 if(idx == item) { 8232 child.showing(true, false); 8233 if(parentWindow) { 8234 auto f = parentWindow.getFirstFocusable(child); 8235 if(f) 8236 f.focus(); 8237 } 8238 recomputeChildLayout(); 8239 } 8240 } 8241 8242 version(win32_widgets) { 8243 InvalidateRect(hwnd, null, true); 8244 } else version(custom_widgets) { 8245 this.redraw(); 8246 } 8247 } 8248 8249 } 8250 8251 /++ 8252 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 8253 8254 You add [TabWidgetPage]s to it. 8255 +/ 8256 class PageWidget : Widget { 8257 this(Widget parent) { 8258 super(parent); 8259 } 8260 8261 override int minHeight() { 8262 int max = 0; 8263 foreach(child; children) 8264 max = mymax(child.minHeight, max); 8265 8266 return max; 8267 } 8268 8269 8270 override void addChild(Widget child, int pos = int.max) { 8271 if(auto twp = cast(TabWidgetPage) child) { 8272 super.addChild(child, pos); 8273 if(pos == int.max) 8274 pos = cast(int) this.children.length - 1; 8275 8276 if(pos != getCurrentTab) { 8277 child.showing = false; 8278 } 8279 } else { 8280 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 8281 } 8282 } 8283 8284 override void recomputeChildLayout() { 8285 this.registerMovement(); 8286 foreach(child; children) { 8287 child.x = 0; 8288 child.y = 0; 8289 child.width = width; 8290 child.height = height; 8291 child.recomputeChildLayout(); 8292 } 8293 } 8294 8295 private int currentTab_; 8296 8297 /// 8298 @scriptable 8299 void setCurrentTab(int item) { 8300 currentTab_ = item; 8301 8302 showOnly(item); 8303 } 8304 8305 /// 8306 @scriptable 8307 int getCurrentTab() { 8308 return currentTab_; 8309 } 8310 8311 /// 8312 @scriptable 8313 void removeTab(int item) { 8314 if(item && item == getCurrentTab()) 8315 setCurrentTab(item - 1); 8316 8317 for(int a = item; a < children.length - 1; a++) 8318 this._children[a] = this._children[a + 1]; 8319 this._children = this._children[0 .. $-1]; 8320 } 8321 8322 /// 8323 @scriptable 8324 TabWidgetPage addPage(string title) { 8325 return new TabWidgetPage(title, this); 8326 } 8327 8328 private void showOnly(int item) { 8329 foreach(idx, child; children) 8330 if(idx == item) { 8331 child.show(); 8332 child.queueRecomputeChildLayout(); 8333 } else { 8334 child.hide(); 8335 } 8336 } 8337 } 8338 8339 /++ 8340 8341 +/ 8342 class TabWidgetPage : Widget { 8343 this(string title, Widget parent) { 8344 this.title_ = title; 8345 this.tabStop = false; 8346 super(parent); 8347 8348 ///* 8349 version(win32_widgets) { 8350 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 8351 } 8352 //*/ 8353 } 8354 8355 private string title_; 8356 8357 /++ 8358 History: 8359 Prior to April 6, 2025, it was a public field. It was changed to properties so it can queue redraws; 8360 +/ 8361 string title() { 8362 return title_; 8363 } 8364 8365 /// ditto 8366 void title(string t) { 8367 title_ = t; 8368 version(custom_widgets) { 8369 if(auto tw = cast(TabWidget) parent) { 8370 foreach(idx, child; tw.children) 8371 if(child is this) 8372 tw.tabs[idx] = t; 8373 tw.redraw(); 8374 } 8375 } 8376 } 8377 8378 override int minHeight() { 8379 int sum = 0; 8380 foreach(child; children) 8381 sum += child.minHeight(); 8382 return sum; 8383 } 8384 } 8385 8386 version(none) 8387 /++ 8388 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 8389 8390 I think I need to modify the layout algorithms to support this. 8391 +/ 8392 class CollapsableSidebar : Widget { 8393 8394 } 8395 8396 /// Stacks the widgets vertically, taking all the available width for each child. 8397 class VerticalLayout : Layout { 8398 // most of this is intentionally blank - widget's default is vertical layout right now 8399 /// 8400 this(Widget parent) { super(parent); } 8401 8402 /++ 8403 Sets a max width for the layout so you don't have to subclass. The max width 8404 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8405 8406 History: 8407 Added November 29, 2021 (dub v10.5) 8408 +/ 8409 this(int maxWidth, Widget parent) { 8410 this.mw = maxWidth; 8411 super(parent); 8412 } 8413 8414 private int mw = int.max; 8415 8416 override int maxWidth() { return scaleWithDpi(mw); } 8417 } 8418 8419 /// Stacks the widgets horizontally, taking all the available height for each child. 8420 class HorizontalLayout : Layout { 8421 /// 8422 this(Widget parent) { super(parent); } 8423 8424 /++ 8425 Sets a max height for the layout so you don't have to subclass. The max height 8426 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8427 8428 History: 8429 Added November 29, 2021 (dub v10.5) 8430 +/ 8431 this(int maxHeight, Widget parent) { 8432 this.mh = maxHeight; 8433 super(parent); 8434 } 8435 8436 private int mh = 0; 8437 8438 8439 8440 override void recomputeChildLayout() { 8441 .recomputeChildLayout!"width"(this); 8442 } 8443 8444 override int minHeight() { 8445 int largest = 0; 8446 int margins = 0; 8447 int lastMargin = 0; 8448 foreach(child; children) { 8449 auto mh = child.minHeight(); 8450 if(mh > largest) 8451 largest = mh; 8452 margins += mymax(lastMargin, child.marginTop()); 8453 lastMargin = child.marginBottom(); 8454 } 8455 return largest + margins; 8456 } 8457 8458 override int maxHeight() { 8459 if(mh != 0) 8460 return mymax(minHeight, scaleWithDpi(mh)); 8461 8462 int largest = 0; 8463 int margins = 0; 8464 int lastMargin = 0; 8465 foreach(child; children) { 8466 auto mh = child.maxHeight(); 8467 if(mh == int.max) 8468 return int.max; 8469 if(mh > largest) 8470 largest = mh; 8471 margins += mymax(lastMargin, child.marginTop()); 8472 lastMargin = child.marginBottom(); 8473 } 8474 return largest + margins; 8475 } 8476 8477 override int heightStretchiness() { 8478 int max; 8479 foreach(child; children) { 8480 auto c = child.heightStretchiness; 8481 if(c > max) 8482 max = c; 8483 } 8484 return max; 8485 } 8486 } 8487 8488 version(win32_widgets) 8489 private 8490 extern(Windows) 8491 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 8492 Widget* pwin = hwnd in Widget.nativeMapping; 8493 if(pwin is null) 8494 return DefWindowProc(hwnd, message, wparam, lparam); 8495 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 8496 if(win is null) 8497 return DefWindowProc(hwnd, message, wparam, lparam); 8498 8499 switch(message) { 8500 case WM_SIZE: 8501 auto width = LOWORD(lparam); 8502 auto height = HIWORD(lparam); 8503 8504 auto hdc = GetDC(hwnd); 8505 auto hdcBmp = CreateCompatibleDC(hdc); 8506 8507 // FIXME: could this be more efficient? it never relinquishes a large bitmap 8508 if(width > win.bmpWidth || height > win.bmpHeight) { 8509 auto oldBuffer = win.buffer; 8510 win.buffer = CreateCompatibleBitmap(hdc, width, height); 8511 8512 if(oldBuffer) 8513 DeleteObject(oldBuffer); 8514 8515 win.bmpWidth = width; 8516 win.bmpHeight = height; 8517 } 8518 8519 // just always erase it upon resizing so minigui can draw over with a clean slate 8520 auto oldBmp = SelectObject(hdcBmp, win.buffer); 8521 8522 auto brush = GetSysColorBrush(COLOR_3DFACE); 8523 RECT r; 8524 r.left = 0; 8525 r.top = 0; 8526 r.right = width; 8527 r.bottom = height; 8528 FillRect(hdcBmp, &r, brush); 8529 8530 SelectObject(hdcBmp, oldBmp); 8531 DeleteDC(hdcBmp); 8532 ReleaseDC(hwnd, hdc); 8533 break; 8534 case WM_PAINT: 8535 if(win.buffer is null) 8536 goto default; 8537 8538 BITMAP bm; 8539 PAINTSTRUCT ps; 8540 8541 HDC hdc = BeginPaint(hwnd, &ps); 8542 8543 HDC hdcMem = CreateCompatibleDC(hdc); 8544 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 8545 8546 GetObject(win.buffer, bm.sizeof, &bm); 8547 8548 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 8549 8550 SelectObject(hdcMem, hbmOld); 8551 DeleteDC(hdcMem); 8552 EndPaint(hwnd, &ps); 8553 break; 8554 default: 8555 return DefWindowProc(hwnd, message, wparam, lparam); 8556 } 8557 8558 return 0; 8559 } 8560 8561 private wstring Win32Class(wstring name)() { 8562 static bool classRegistered; 8563 if(!classRegistered) { 8564 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 8565 WNDCLASSEX wc; 8566 wc.cbSize = wc.sizeof; 8567 wc.hInstance = hInstance; 8568 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 8569 wc.lpfnWndProc = &DoubleBufferWndProc; 8570 wc.lpszClassName = name.ptr; 8571 if(!RegisterClassExW(&wc)) 8572 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 8573 classRegistered = true; 8574 } 8575 8576 return name; 8577 } 8578 8579 /+ 8580 version(win32_widgets) 8581 extern(Windows) 8582 private 8583 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 8584 switch(iMessage) { 8585 case WM_PAINT: 8586 if(auto te = hWnd in Widget.nativeMapping) { 8587 try { 8588 //te.redraw(); 8589 writeln(te, " drawing"); 8590 } catch(Exception) {} 8591 } 8592 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8593 default: 8594 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8595 } 8596 } 8597 +/ 8598 8599 8600 /++ 8601 A widget specifically designed to hold other widgets. 8602 8603 History: 8604 Added July 1, 2021 8605 +/ 8606 class ContainerWidget : Widget { 8607 this(Widget parent) { 8608 super(parent); 8609 this.tabStop = false; 8610 8611 version(win32_widgets) { 8612 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 8613 } 8614 } 8615 } 8616 8617 /++ 8618 A widget that takes your widget, puts scroll bars around it, and sends 8619 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 8620 no effort to automatically scroll or clip its child widgets - it just sends 8621 the messages. 8622 8623 8624 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 8625 The scroll coordinates are all given in a unit you interpret as you wish. One 8626 of these units is moved on each press of the arrow buttons and represents the 8627 smallest amount the user can scroll. The intention is for this to be one line, 8628 one item in a list, one row in a table, etc. Whatever makes sense for your widget 8629 in each direction that the user might be interested in. 8630 8631 You can set a "page size" with the [step] property. (Yes, I regret the name...) 8632 This is the amount it jumps when the user pressed page up and page down, or clicks 8633 in the exposed part of the scroll bar. 8634 8635 You should add child content to the ScrollMessageWidget. However, it is important to 8636 note that the coordinates are always independent of the scroll position! It is YOUR 8637 responsibility to do any necessary transforms, clipping, etc., while drawing the 8638 content and interpreting mouse events if they are supposed to change with the scroll. 8639 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 8640 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 8641 you more control (which can be considerably more efficient and adapted to your actual data) 8642 at the expense of you also needing to be aware of its reality. 8643 8644 Please note that it does NOT react to mouse wheel events or various keyboard events as of 8645 version 10.3. Maybe this will change in the future.... but for now you must call 8646 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 8647 +/ 8648 class ScrollMessageWidget : Widget { 8649 this(Widget parent) { 8650 super(parent); 8651 8652 container = new Widget(this); 8653 hsb = new HorizontalScrollbar(this); 8654 vsb = new VerticalScrollbar(this); 8655 8656 hsb.addEventListener("scrolltonextline", { 8657 hsb.setPosition(hsb.position + movementPerButtonClickH_); 8658 notify(); 8659 }); 8660 hsb.addEventListener("scrolltopreviousline", { 8661 hsb.setPosition(hsb.position - movementPerButtonClickH_); 8662 notify(); 8663 }); 8664 vsb.addEventListener("scrolltonextline", { 8665 vsb.setPosition(vsb.position + movementPerButtonClickV_); 8666 notify(); 8667 }); 8668 vsb.addEventListener("scrolltopreviousline", { 8669 vsb.setPosition(vsb.position - movementPerButtonClickV_); 8670 notify(); 8671 }); 8672 hsb.addEventListener("scrolltonextpage", { 8673 hsb.setPosition(hsb.position + hsb.step_); 8674 notify(); 8675 }); 8676 hsb.addEventListener("scrolltopreviouspage", { 8677 hsb.setPosition(hsb.position - hsb.step_); 8678 notify(); 8679 }); 8680 vsb.addEventListener("scrolltonextpage", { 8681 vsb.setPosition(vsb.position + vsb.step_); 8682 notify(); 8683 }); 8684 vsb.addEventListener("scrolltopreviouspage", { 8685 vsb.setPosition(vsb.position - vsb.step_); 8686 notify(); 8687 }); 8688 hsb.addEventListener("scrolltoposition", (Event event) { 8689 hsb.setPosition(event.intValue); 8690 notify(); 8691 }); 8692 vsb.addEventListener("scrolltoposition", (Event event) { 8693 vsb.setPosition(event.intValue); 8694 notify(); 8695 }); 8696 8697 8698 tabStop = false; 8699 container.tabStop = false; 8700 magic = true; 8701 } 8702 8703 private int movementPerButtonClickH_ = 1; 8704 private int movementPerButtonClickV_ = 1; 8705 public void movementPerButtonClick(int h, int v) { 8706 movementPerButtonClickH_ = h; 8707 movementPerButtonClickV_ = v; 8708 } 8709 8710 /++ 8711 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 8712 8713 8714 The defaults for [addDefaultWheelListeners] are: 8715 8716 $(LIST 8717 * Mouse wheel scrolls vertically 8718 * Alt key + mouse wheel scrolls horiontally 8719 * Shift + mouse wheel scrolls faster. 8720 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 8721 ) 8722 8723 The defaults for [addDefaultKeyboardListeners] are: 8724 8725 $(LIST 8726 * Arrow keys scroll by the given amounts 8727 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 8728 * Page up and down scroll by the vertical viewable area 8729 * Home and end scroll to the start and end of the verticle viewable area. 8730 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 8731 ) 8732 8733 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 8734 8735 Params: 8736 horizontalArrowScrollAmount = 8737 verticalArrowScrollAmount = 8738 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 8739 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 8740 shiftMultiplier = multiplies the scroll amount by this when shift is held 8741 +/ 8742 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 8743 defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount; 8744 defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount; 8745 defaultKeyboardListener_shiftMultiplier = shiftMultiplier; 8746 8747 container.addEventListener(&defaultKeyboardListener); 8748 } 8749 8750 /// ditto 8751 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 8752 auto _this = this; 8753 container.addEventListener((scope ClickEvent ce) { 8754 8755 //if(ce.target && ce.target.tabStop) 8756 //ce.target.focus(); 8757 8758 // ctrl is reserved for the application 8759 if(ce.ctrlKey) 8760 return; 8761 8762 if(horizontalWheelScrollAmount == 0 && ce.altKey) 8763 return; 8764 8765 if(shiftMultiplier == 0 && ce.shiftKey) 8766 return; 8767 8768 if(ce.button == MouseButton.wheelDown) { 8769 if(ce.altKey) 8770 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8771 else 8772 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8773 } else if(ce.button == MouseButton.wheelUp) { 8774 if(ce.altKey) 8775 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8776 else 8777 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8778 } 8779 }); 8780 } 8781 8782 int defaultKeyboardListener_verticalArrowScrollAmount = 1; 8783 int defaultKeyboardListener_horizontalArrowScrollAmount = 1; 8784 int defaultKeyboardListener_shiftMultiplier = 3; 8785 8786 void defaultKeyboardListener(scope KeyDownEvent ke) { 8787 switch(ke.key) { 8788 case Key.Left: 8789 this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8790 break; 8791 case Key.Right: 8792 this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8793 break; 8794 case Key.Up: 8795 this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8796 break; 8797 case Key.Down: 8798 this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8799 break; 8800 case Key.PageUp: 8801 if(ke.altKey) 8802 this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8803 else 8804 this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8805 break; 8806 case Key.PageDown: 8807 if(ke.altKey) 8808 this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8809 else 8810 this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8811 break; 8812 case Key.Home: 8813 if(ke.altKey) 8814 this.scrollLeft(short.max * 16); 8815 else 8816 this.scrollUp(short.max * 16); 8817 break; 8818 case Key.End: 8819 if(ke.altKey) 8820 this.scrollRight(short.max * 16); 8821 else 8822 this.scrollDown(short.max * 16); 8823 break; 8824 8825 default: 8826 // ignore, not for us. 8827 } 8828 } 8829 8830 /++ 8831 Scrolls the given amount. 8832 8833 History: 8834 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. 8835 +/ 8836 void scrollUp(int amount = 1) { 8837 vsb.setPosition(vsb.position.NonOverflowingInt - amount); 8838 notify(); 8839 } 8840 /// ditto 8841 void scrollDown(int amount = 1) { 8842 vsb.setPosition(vsb.position.NonOverflowingInt + amount); 8843 notify(); 8844 } 8845 /// ditto 8846 void scrollLeft(int amount = 1) { 8847 hsb.setPosition(hsb.position.NonOverflowingInt - amount); 8848 notify(); 8849 } 8850 /// ditto 8851 void scrollRight(int amount = 1) { 8852 hsb.setPosition(hsb.position.NonOverflowingInt + amount); 8853 notify(); 8854 } 8855 8856 /// 8857 VerticalScrollbar verticalScrollBar() { return vsb; } 8858 /// 8859 HorizontalScrollbar horizontalScrollBar() { return hsb; } 8860 8861 void notify() { 8862 static bool insideNotify; 8863 8864 if(insideNotify) 8865 return; // avoid the recursive call, even if it isn't strictly correct 8866 8867 insideNotify = true; 8868 scope(exit) insideNotify = false; 8869 8870 this.emit!ScrollEvent(); 8871 } 8872 8873 mixin Emits!ScrollEvent; 8874 8875 /// 8876 Point position() { 8877 return Point(hsb.position, vsb.position); 8878 } 8879 8880 /// 8881 void setPosition(int x, int y) { 8882 hsb.setPosition(x); 8883 vsb.setPosition(y); 8884 } 8885 8886 /// 8887 void setPageSize(int unitsX, int unitsY) { 8888 hsb.setStep(unitsX); 8889 vsb.setStep(unitsY); 8890 } 8891 8892 /// Always call this BEFORE setViewableArea 8893 void setTotalArea(int width, int height) { 8894 hsb.setMax(width); 8895 vsb.setMax(height); 8896 } 8897 8898 /++ 8899 Always set the viewable area AFTER setitng the total area if you are going to change both. 8900 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 8901 If you need to do that, use [queueRecomputeChildLayout]. 8902 +/ 8903 void setViewableArea(int width, int height) { 8904 8905 // actually there IS A need to dothis cuz the max might have changed since then 8906 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 8907 //return; // no need to do what is already done 8908 hsb.setViewableArea(width); 8909 vsb.setViewableArea(height); 8910 8911 bool needsNotify = false; 8912 8913 // FIXME: if at any point the rhs is outside the scrollbar, we need 8914 // to reset to 0. but it should remember the old position in case the 8915 // window resizes again, so it can kinda return ot where it was. 8916 // 8917 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 8918 if(width >= hsb.max) { 8919 // there's plenty of room to display it all so we need to reset to zero 8920 // FIXME: adjust so it matches the note above 8921 hsb.setPosition(0); 8922 needsNotify = true; 8923 } 8924 if(height >= vsb.max) { 8925 // there's plenty of room to display it all so we need to reset to zero 8926 // FIXME: adjust so it matches the note above 8927 vsb.setPosition(0); 8928 needsNotify = true; 8929 } 8930 if(needsNotify) 8931 notify(); 8932 } 8933 8934 private bool magic; 8935 override void addChild(Widget w, int position = int.max) { 8936 if(magic) 8937 container.addChild(w, position); 8938 else 8939 super.addChild(w, position); 8940 } 8941 8942 override void recomputeChildLayout() { 8943 if(hsb is null || vsb is null || container is null) return; 8944 8945 registerMovement(); 8946 8947 enum BUTTON_SIZE = 16; 8948 8949 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 8950 hsb.x = 0; 8951 hsb.y = this.height - hsb.height; 8952 8953 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 8954 vsb.x = this.width - vsb.width; 8955 vsb.y = 0; 8956 8957 auto vsb_width = vsb.showing ? vsb.width : 0; 8958 auto hsb_height = hsb.showing ? hsb.height : 0; 8959 8960 hsb.width = this.width - vsb_width; 8961 vsb.height = this.height - hsb_height; 8962 8963 hsb.recomputeChildLayout(); 8964 vsb.recomputeChildLayout(); 8965 8966 if(this.header is null) { 8967 container.x = 0; 8968 container.y = 0; 8969 container.width = this.width - vsb_width; 8970 container.height = this.height - hsb_height; 8971 container.recomputeChildLayout(); 8972 } else { 8973 header.x = 0; 8974 header.y = 0; 8975 header.width = this.width - vsb_width; 8976 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 8977 header.recomputeChildLayout(); 8978 8979 container.x = 0; 8980 container.y = scaleWithDpi(BUTTON_SIZE); 8981 container.width = this.width - vsb_width; 8982 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 8983 container.recomputeChildLayout(); 8984 } 8985 } 8986 8987 private HorizontalScrollbar hsb; 8988 private VerticalScrollbar vsb; 8989 Widget container; 8990 private Widget header; 8991 8992 /++ 8993 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 8994 8995 History: 8996 Added September 27, 2021 (dub v10.3) 8997 +/ 8998 Widget getHeader() { 8999 if(this.header is null) { 9000 magic = false; 9001 scope(exit) magic = true; 9002 this.header = new Widget(this); 9003 queueRecomputeChildLayout(); 9004 } 9005 return this.header; 9006 } 9007 9008 /++ 9009 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 9010 9011 History: 9012 Added January 3, 2023 (dub v11.0) 9013 +/ 9014 void scrollIntoView(Rectangle rect) { 9015 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 9016 9017 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 9018 9019 // the lower right is exclusive normally 9020 auto test = rect.lowerRight; 9021 if(test.x > 0) test.x--; 9022 if(test.y > 0) test.y--; 9023 9024 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 9025 // try to scroll only one dimension at a time if we can 9026 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 9027 setPosition(rect.upperLeft.x, position.y); 9028 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 9029 setPosition(position.x, rect.upperLeft.y); 9030 } 9031 9032 } 9033 9034 override int minHeight() { 9035 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 9036 if(header !is null) 9037 min += header.minHeight; 9038 if(horizontalScrollBar.showing) 9039 min += horizontalScrollBar.minHeight; 9040 return min; 9041 } 9042 9043 override int maxHeight() { 9044 int max = container ? container.maxHeight : int.max; 9045 if(max == int.max) 9046 return max; 9047 if(horizontalScrollBar.showing) 9048 max += horizontalScrollBar.minHeight; 9049 return max; 9050 } 9051 9052 static class Style : Widget.Style { 9053 override WidgetBackground background() { 9054 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 9055 } 9056 } 9057 mixin OverrideStyle!Style; 9058 } 9059 9060 /++ 9061 $(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") 9062 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 9063 +/ 9064 version(minigui_screenshots) 9065 @Screenshot("ScrollMessageWidget") 9066 unittest { 9067 auto window = new Window("ScrollMessageWidget"); 9068 9069 auto smw = new ScrollMessageWidget(window); 9070 smw.addDefaultKeyboardListeners(); 9071 smw.addDefaultWheelListeners(); 9072 9073 window.loop(); 9074 } 9075 9076 /++ 9077 Bypasses automatic layout for its children, using manual positioning and sizing only. 9078 While you need to manually position them, you must ensure they are inside the StaticLayout's 9079 bounding box to avoid undefined behavior. 9080 9081 You should almost never use this. 9082 +/ 9083 class StaticLayout : Layout { 9084 /// 9085 this(Widget parent) { super(parent); } 9086 override void recomputeChildLayout() { 9087 registerMovement(); 9088 foreach(child; children) 9089 child.recomputeChildLayout(); 9090 } 9091 } 9092 9093 /++ 9094 Bypasses automatic positioning when being laid out. It is your responsibility to make 9095 room for this widget in the parent layout. 9096 9097 Its children are laid out normally, unless there is exactly one, in which case it takes 9098 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 9099 can do that with `padding`). 9100 +/ 9101 class StaticPosition : Layout { 9102 /// 9103 this(Widget parent) { super(parent); } 9104 9105 override void recomputeChildLayout() { 9106 registerMovement(); 9107 if(this.children.length == 1) { 9108 auto child = children[0]; 9109 child.x = 0; 9110 child.y = 0; 9111 child.width = this.width; 9112 child.height = this.height; 9113 child.recomputeChildLayout(); 9114 } else 9115 foreach(child; children) 9116 child.recomputeChildLayout(); 9117 } 9118 9119 alias width = typeof(super).width; 9120 alias height = typeof(super).height; 9121 9122 @property int width(int w) @nogc pure @safe nothrow { 9123 return this._width = w; 9124 } 9125 9126 @property int height(int w) @nogc pure @safe nothrow { 9127 return this._height = w; 9128 } 9129 9130 } 9131 9132 /++ 9133 FixedPosition is like [StaticPosition], but its coordinates 9134 are always relative to the viewport, meaning they do not scroll with 9135 the parent content. 9136 +/ 9137 class FixedPosition : StaticPosition { 9138 /// 9139 this(Widget parent) { super(parent); } 9140 } 9141 9142 version(win32_widgets) 9143 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 9144 if(true) { 9145 // cmd == 0 = menu, cmd == 1 = accelerator 9146 if(auto item = idm in Action.mapping) { 9147 foreach(handler; (*item).triggered) 9148 handler(); 9149 /* 9150 auto event = new Event("triggered", *item); 9151 event.button = idm; 9152 event.dispatch(); 9153 */ 9154 return 0; 9155 } 9156 } 9157 if(handle) 9158 if(auto widgetp = handle in Widget.nativeMapping) { 9159 (*widgetp).handleWmCommand(cmd, idm); 9160 return 0; 9161 } 9162 return 1; 9163 } 9164 9165 9166 /// 9167 class Window : Widget { 9168 Widget[] mouseCapturedBy; 9169 void captureMouse(Widget byWhom) { 9170 assert(byWhom !is null); 9171 if(mouseCapturedBy.length > 0) { 9172 auto cc = mouseCapturedBy[$-1]; 9173 if(cc is byWhom) 9174 return; // or should it throw? 9175 auto par = byWhom; 9176 while(par) { 9177 if(cc is par) 9178 goto allowed; 9179 par = par.parent; 9180 } 9181 9182 throw new Exception("mouse is already captured by other widget"); 9183 } 9184 allowed: 9185 mouseCapturedBy ~= byWhom; 9186 if(mouseCapturedBy.length == 1) 9187 win.grabInput(false, true, false); 9188 //void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) { 9189 } 9190 void releaseMouseCapture() { 9191 if(mouseCapturedBy.length == 0) 9192 return; // or should it throw? 9193 mouseCapturedBy = mouseCapturedBy[0 .. $-1]; 9194 mouseCapturedBy.assumeSafeAppend(); 9195 if(mouseCapturedBy.length == 0) 9196 win.releaseInputGrab(); 9197 } 9198 9199 9200 /++ 9201 9202 +/ 9203 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 9204 return .messageBox(this, title, message, style, icon); 9205 } 9206 9207 /// ditto 9208 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 9209 return messageBox(null, message, style, icon); 9210 } 9211 9212 9213 /++ 9214 Sets the window icon which is often seen in title bars and taskbars. 9215 9216 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. 9217 9218 History: 9219 Added April 5, 2022 (dub v10.8) 9220 +/ 9221 @property void icon(MemoryImage icon) { 9222 if(win && icon) 9223 win.icon = icon; 9224 } 9225 9226 // 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 9227 // this does NOT change the icon on the window! That's what the other overload is for 9228 static @property .icon icon(GenericIcons i) { 9229 return .icon(i); 9230 } 9231 9232 /// 9233 @scriptable 9234 @property bool focused() { 9235 return win.focused; 9236 } 9237 9238 static class Style : Widget.Style { 9239 override WidgetBackground background() { 9240 version(custom_widgets) 9241 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 9242 else version(win32_widgets) 9243 return WidgetBackground(Color.transparent); 9244 else static assert(0); 9245 } 9246 } 9247 mixin OverrideStyle!Style; 9248 9249 /++ 9250 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. 9251 +/ 9252 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 9253 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 9254 } 9255 9256 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 9257 OperatingSystemFont font; 9258 if(auto vt = WidgetPainter.visualTheme) { 9259 font = vt.defaultFontCached(96); // FIXME 9260 } 9261 9262 if(font is null) { 9263 static int defaultHeightCache; 9264 if(defaultHeightCache == 0) { 9265 font = new OperatingSystemFont; 9266 font.loadDefault; 9267 defaultHeightCache = castFnumToCnum(font.height());// * 5 / 4; 9268 } 9269 return defaultHeightCache; 9270 } 9271 9272 return castFnumToCnum(font.height());// * 5 / 4; 9273 } 9274 9275 Widget focusedWidget; 9276 9277 private SimpleWindow win_; 9278 9279 @property { 9280 /++ 9281 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 9282 9283 History: 9284 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 9285 +/ 9286 public SimpleWindow win() { 9287 return win_; 9288 } 9289 /// 9290 protected void win(SimpleWindow w) { 9291 win_ = w; 9292 } 9293 } 9294 9295 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 9296 this(Widget p) { 9297 tabStop = false; 9298 super(p); 9299 } 9300 9301 private void actualRedraw() { 9302 if(recomputeChildLayoutRequired) 9303 recomputeChildLayoutEntry(); 9304 if(!showing) return; 9305 9306 assert(parentWindow !is null); 9307 9308 auto w = drawableWindow; 9309 if(w is null) 9310 w = parentWindow.win; 9311 9312 if(w.closed()) 9313 return; 9314 9315 auto ugh = this.parent; 9316 int lox, loy; 9317 while(ugh) { 9318 lox += ugh.x; 9319 loy += ugh.y; 9320 ugh = ugh.parent; 9321 } 9322 auto painter = w.draw(true); 9323 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 9324 } 9325 9326 9327 private bool skipNextChar = false; 9328 9329 /++ 9330 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 9331 9332 This constructor is intended primarily for internal use and may be changed to `protected` later. 9333 +/ 9334 this(SimpleWindow win) { 9335 9336 static if(UsingSimpledisplayX11) { 9337 win.discardAdditionalConnectionState = &discardXConnectionState; 9338 win.recreateAdditionalConnectionState = &recreateXConnectionState; 9339 } 9340 9341 tabStop = false; 9342 super(null); 9343 this.win = win; 9344 9345 win.addEventListener((Widget.RedrawEvent) { 9346 if(win.eventQueued!RecomputeEvent) { 9347 // writeln("skipping"); 9348 return; // let the recompute event do the actual redraw 9349 } 9350 this.actualRedraw(); 9351 }); 9352 9353 win.addEventListener((Widget.RecomputeEvent) { 9354 recomputeChildLayoutEntry(); 9355 if(win.eventQueued!RedrawEvent) 9356 return; // let the queued one do it 9357 else { 9358 // writeln("drawing"); 9359 this.actualRedraw(); // if not queued, it needs to be done now anyway 9360 } 9361 }); 9362 9363 this.width = win.width; 9364 this.height = win.height; 9365 this.parentWindow = this; 9366 9367 win.closeQuery = () { 9368 if(this.emit!ClosingEvent()) 9369 win.close(); 9370 }; 9371 win.onClosing = () { 9372 this.emit!ClosedEvent(); 9373 }; 9374 9375 win.windowResized = (int w, int h) { 9376 this.width = w; 9377 this.height = h; 9378 queueRecomputeChildLayout(); 9379 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 9380 //version(win32_widgets) 9381 //InvalidateRect(hwnd, null, true); 9382 redraw(); 9383 }; 9384 9385 win.onFocusChange = (bool getting) { 9386 // sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString); 9387 if(this.focusedWidget) { 9388 if(getting) { 9389 this.focusedWidget.emit!FocusEvent(); 9390 this.focusedWidget.emit!FocusInEvent(); 9391 } else { 9392 this.focusedWidget.emit!BlurEvent(); 9393 this.focusedWidget.emit!FocusOutEvent(); 9394 } 9395 } 9396 9397 if(getting) { 9398 this.emit!FocusEvent(); 9399 this.emit!FocusInEvent(); 9400 } else { 9401 this.emit!BlurEvent(); 9402 this.emit!FocusOutEvent(); 9403 } 9404 }; 9405 9406 win.onDpiChanged = { 9407 this.queueRecomputeChildLayout(); 9408 auto event = new DpiChangedEvent(this); 9409 event.sendDirectly(); 9410 9411 privateDpiChanged(); 9412 }; 9413 9414 win.setEventHandlers( 9415 (MouseEvent e) { 9416 dispatchMouseEvent(e); 9417 }, 9418 (KeyEvent e) { 9419 //writefln("%x %s", cast(uint) e.key, e.key); 9420 dispatchKeyEvent(e); 9421 }, 9422 (dchar e) { 9423 if(e == 13) e = 10; // hack? 9424 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9425 dispatchCharEvent(e); 9426 }, 9427 ); 9428 9429 addEventListener("char", (Widget, Event ev) { 9430 if(skipNextChar) { 9431 ev.preventDefault(); 9432 skipNextChar = false; 9433 } 9434 }); 9435 9436 version(win32_widgets) 9437 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 9438 if(hwnd !is this.win.impl.hwnd) 9439 return 1; // we don't care... pass it on 9440 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 9441 if(mustReturn) 9442 return ret; 9443 return 1; // pass it on 9444 }; 9445 9446 if(Window.newWindowCreated) 9447 Window.newWindowCreated(this); 9448 } 9449 9450 version(custom_widgets) 9451 override void defaultEventHandler_click(ClickEvent event) { 9452 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 9453 if(event.target && event.target.tabStop) 9454 event.target.focus(); 9455 } 9456 } 9457 9458 private static void delegate(Window) newWindowCreated; 9459 9460 version(win32_widgets) 9461 override void paint(WidgetPainter painter) { 9462 /* 9463 RECT rect; 9464 rect.right = this.width; 9465 rect.bottom = this.height; 9466 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 9467 */ 9468 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 9469 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 9470 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 9471 // since the pen is null, to fill the whole space, we need the +1 on both. 9472 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 9473 SelectObject(painter.impl.hdc, p); 9474 SelectObject(painter.impl.hdc, b); 9475 } 9476 version(custom_widgets) 9477 override void paint(WidgetPainter painter) { 9478 auto cs = getComputedStyle(); 9479 painter.fillColor = cs.windowBackgroundColor; 9480 painter.outlineColor = cs.windowBackgroundColor; 9481 painter.drawRectangle(Point(0, 0), this.width, this.height); 9482 } 9483 9484 9485 override void defaultEventHandler_keydown(KeyDownEvent event) { 9486 Widget _this = event.target; 9487 9488 if(event.key == Key.Tab) { 9489 /* Window tab ordering is a recursive thingy with each group */ 9490 9491 // FIXME inefficient 9492 Widget[] helper(Widget p) { 9493 if(p.hidden) 9494 return null; 9495 Widget[] childOrdering; 9496 9497 auto children = p.children.dup; 9498 9499 while(true) { 9500 // UIs should be generally small, so gonna brute force it a little 9501 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 9502 9503 Widget smallestTab; 9504 foreach(ref c; children) { 9505 if(c is null) continue; 9506 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 9507 smallestTab = c; 9508 c = null; 9509 } 9510 } 9511 if(smallestTab !is null) { 9512 if(smallestTab.tabStop && !smallestTab.hidden) 9513 childOrdering ~= smallestTab; 9514 if(!smallestTab.hidden) 9515 childOrdering ~= helper(smallestTab); 9516 } else 9517 break; 9518 9519 } 9520 9521 return childOrdering; 9522 } 9523 9524 Widget[] tabOrdering = helper(this); 9525 9526 Widget recipient; 9527 9528 if(tabOrdering.length) { 9529 bool seenThis = false; 9530 Widget previous; 9531 foreach(idx, child; tabOrdering) { 9532 if(child is focusedWidget) { 9533 9534 if(event.shiftKey) { 9535 if(idx == 0) 9536 recipient = tabOrdering[$-1]; 9537 else 9538 recipient = tabOrdering[idx - 1]; 9539 break; 9540 } 9541 9542 seenThis = true; 9543 if(idx + 1 == tabOrdering.length) { 9544 // we're at the end, either move to the next group 9545 // or start back over 9546 recipient = tabOrdering[0]; 9547 } 9548 continue; 9549 } 9550 if(seenThis) { 9551 recipient = child; 9552 break; 9553 } 9554 previous = child; 9555 } 9556 } 9557 9558 if(recipient !is null) { 9559 // writeln(typeid(recipient)); 9560 recipient.focus(); 9561 9562 skipNextChar = true; 9563 } 9564 } 9565 9566 debug if(event.key == Key.F12) { 9567 if(devTools) { 9568 devTools.close(); 9569 devTools = null; 9570 } else { 9571 devTools = new DevToolWindow(this); 9572 devTools.show(); 9573 } 9574 } 9575 } 9576 9577 debug DevToolWindow devTools; 9578 9579 9580 /++ 9581 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 9582 9583 History: 9584 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 9585 9586 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 9587 +/ 9588 this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9589 if(title is null) { 9590 import core.runtime; 9591 if(Runtime.args.length) 9592 title = Runtime.args[0]; 9593 } 9594 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent); 9595 9596 static if(UsingSimpledisplayX11) 9597 if(windowFlags & WindowFlags.managesChildWindowFocus) { 9598 ///+ 9599 // for input proxy 9600 auto display = XDisplayConnection.get; 9601 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 9602 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 9603 XMapWindow(display, inputProxy); 9604 // writefln("input proxy: 0x%0x", inputProxy); 9605 this.inputProxy = new SimpleWindow(inputProxy); 9606 9607 /+ 9608 this.inputProxy.onFocusChange = (bool getting) { 9609 sdpyPrintDebugString("input proxy focus change ", getting); 9610 }; 9611 +/ 9612 9613 XEvent lastEvent; 9614 this.inputProxy.handleNativeEvent = (XEvent ev) { 9615 lastEvent = ev; 9616 return 1; 9617 }; 9618 this.inputProxy.setEventHandlers( 9619 (MouseEvent e) { 9620 dispatchMouseEvent(e); 9621 }, 9622 (KeyEvent e) { 9623 //writefln("%x %s", cast(uint) e.key, e.key); 9624 if(dispatchKeyEvent(e)) { 9625 // FIXME: i should trap error 9626 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 9627 auto thing = nw.focusableWindow(); 9628 if(thing && thing.window) { 9629 lastEvent.xkey.window = thing.window; 9630 // writeln("sending event ", lastEvent.xkey); 9631 trapXErrors( { 9632 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 9633 }); 9634 } 9635 } 9636 } 9637 }, 9638 (dchar e) { 9639 if(e == 13) e = 10; // hack? 9640 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9641 dispatchCharEvent(e); 9642 }, 9643 ); 9644 9645 this.inputProxy.populateXic(); 9646 // done 9647 //+/ 9648 } 9649 9650 9651 9652 win.setRequestedInputFocus = &this.setRequestedInputFocus; 9653 9654 this(win); 9655 } 9656 9657 SimpleWindow inputProxy; 9658 9659 private SimpleWindow setRequestedInputFocus() { 9660 return inputProxy; 9661 } 9662 9663 /// ditto 9664 this(string title, int width = 500, int height = 500) { 9665 this(width, height, title); 9666 } 9667 9668 /// 9669 @property string title() { return parentWindow.win.title; } 9670 /// 9671 @property void title(string title) { parentWindow.win.title = title; } 9672 9673 /// 9674 @scriptable 9675 void close() { 9676 win.close(); 9677 // I synchronize here upon window closing to ensure all child windows 9678 // get updated too before the event loop. This avoids some random X errors. 9679 static if(UsingSimpledisplayX11) { 9680 runInGuiThread( { 9681 XSync(XDisplayConnection.get, false); 9682 }); 9683 } 9684 } 9685 9686 bool dispatchKeyEvent(KeyEvent ev) { 9687 auto wid = focusedWidget; 9688 if(wid is null) 9689 wid = this; 9690 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 9691 event.originalKeyEvent = ev; 9692 event.key = ev.key; 9693 event.state = ev.modifierState; 9694 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9695 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9696 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9697 event.dispatch(); 9698 9699 return !event.propagationStopped; 9700 } 9701 9702 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 9703 bool dispatchCharEvent(dchar ch) { 9704 if(focusedWidget) { 9705 auto event = new CharEvent(focusedWidget, ch); 9706 event.dispatch(); 9707 return !event.propagationStopped; 9708 } 9709 return true; 9710 } 9711 9712 Widget mouseLastOver; 9713 Widget mouseLastDownOn; 9714 bool lastWasDoubleClick; 9715 bool dispatchMouseEvent(MouseEvent ev) { 9716 auto eleR = widgetAtPoint(this, ev.x, ev.y); 9717 auto ele = eleR.widget; 9718 9719 auto captureEle = ele; 9720 9721 auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null; 9722 if(mouseCapturedBy !is null) { 9723 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 9724 captureEle = mouseCapturedBy; 9725 } 9726 9727 // a hack to get it relative to the widget. 9728 eleR.x = ev.x; 9729 eleR.y = ev.y; 9730 auto pain = captureEle; 9731 9732 auto vpx = eleR.x; 9733 auto vpy = eleR.y; 9734 9735 while(pain) { 9736 eleR.x -= pain.x; 9737 eleR.y -= pain.y; 9738 pain.addScrollPosition(eleR.x, eleR.y); 9739 9740 vpx -= pain.x; 9741 vpy -= pain.y; 9742 9743 pain = pain.parent; 9744 } 9745 9746 void populateMouseEventBase(MouseEventBase event) { 9747 event.button = ev.button; 9748 event.buttonLinear = ev.buttonLinear; 9749 event.state = ev.modifierState; 9750 event.clientX = eleR.x; 9751 event.clientY = eleR.y; 9752 9753 event.viewportX = vpx; 9754 event.viewportY = vpy; 9755 9756 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9757 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9758 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9759 } 9760 9761 if(ev.type == MouseEventType.buttonPressed) { 9762 { 9763 auto event = new MouseDownEvent(captureEle); 9764 populateMouseEventBase(event); 9765 event.dispatch(); 9766 } 9767 9768 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 9769 auto event = new DoubleClickEvent(captureEle); 9770 populateMouseEventBase(event); 9771 event.dispatch(); 9772 lastWasDoubleClick = ev.doubleClick; 9773 } else { 9774 lastWasDoubleClick = false; 9775 } 9776 9777 mouseLastDownOn = ele; 9778 } else if(ev.type == MouseEventType.buttonReleased) { 9779 { 9780 auto event = new MouseUpEvent(captureEle); 9781 populateMouseEventBase(event); 9782 event.dispatch(); 9783 } 9784 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 9785 auto event = new ClickEvent(captureEle); 9786 populateMouseEventBase(event); 9787 event.dispatch(); 9788 } 9789 } else if(ev.type == MouseEventType.motion) { 9790 // motion 9791 { 9792 auto event = new MouseMoveEvent(captureEle); 9793 populateMouseEventBase(event); // fills in button which is meaningless but meh 9794 event.dispatch(); 9795 } 9796 9797 if(mouseLastOver !is ele) { 9798 if(ele !is null) { 9799 if(!isAParentOf(ele, mouseLastOver)) { 9800 ele.setDynamicState(DynamicState.hover, true); 9801 auto event = new MouseEnterEvent(ele); 9802 event.relatedTarget = mouseLastOver; 9803 event.sendDirectly(); 9804 9805 ele.useStyleProperties((scope Widget.Style s) { 9806 ele.parentWindow.win.cursor = s.cursor; 9807 }); 9808 } 9809 } 9810 9811 if(mouseLastOver !is null) { 9812 if(!isAParentOf(mouseLastOver, ele)) { 9813 mouseLastOver.setDynamicState(DynamicState.hover, false); 9814 auto event = new MouseLeaveEvent(mouseLastOver); 9815 event.relatedTarget = ele; 9816 event.sendDirectly(); 9817 } 9818 } 9819 9820 if(ele !is null) { 9821 auto event = new MouseOverEvent(ele); 9822 event.relatedTarget = mouseLastOver; 9823 event.dispatch(); 9824 } 9825 9826 if(mouseLastOver !is null) { 9827 auto event = new MouseOutEvent(mouseLastOver); 9828 event.relatedTarget = ele; 9829 event.dispatch(); 9830 } 9831 9832 mouseLastOver = ele; 9833 } 9834 } 9835 9836 return true; // FIXME: the event default prevented? 9837 } 9838 9839 /++ 9840 Shows the window and runs the application event loop. 9841 9842 Blocks until this window is closed. 9843 9844 Bugs: 9845 9846 $(PITFALL 9847 You should always have one event loop live for your application. 9848 If you make two windows in sequence, the second call to loop (or 9849 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 9850 might fail: 9851 9852 --- 9853 // don't do this! 9854 auto window = new Window(); 9855 window.loop(); 9856 9857 // or new Window or new MainWindow, all the same 9858 auto window2 = new SimpleWindow(); 9859 window2.eventLoop(0); // problematic! might crash 9860 --- 9861 9862 simpledisplay's current implementation assumes that final cleanup is 9863 done when the event loop refcount reaches zero. So after the first 9864 eventLoop returns, when there isn't already another one active, it assumes 9865 the program will exit soon and cleans up. 9866 9867 This is arguably a bug that it doesn't reinitialize, and I'll probably change 9868 it eventually, but in the mean time, there's an easy solution: 9869 9870 --- 9871 // do this 9872 EventLoop mainEventLoop = EventLoop.get; // just add this line 9873 9874 auto window = new Window(); 9875 window.loop(); 9876 9877 // or any other type of Window etc. 9878 auto window2 = new Window(); 9879 window2.loop(); // perfectly fine since mainEventLoop still alive 9880 --- 9881 9882 By adding a top-level reference to the event loop, it ensures the final cleanup 9883 is not performed until it goes out of scope too, letting the individual window loops 9884 work without trouble despite the bug. 9885 ) 9886 9887 History: 9888 The [BlockingMode] parameter was added on December 8, 2021. 9889 The default behavior is to block until the application quits 9890 (so all windows have been closed), unless another minigui or 9891 simpledisplay event loop is already running, in which case it 9892 will block until this window closes specifically. 9893 +/ 9894 @scriptable 9895 void loop(BlockingMode bm = BlockingMode.automatic) { 9896 if(win.closed) 9897 return; // otherwise show will throw 9898 show(); 9899 win.eventLoopWithBlockingMode(bm, 0); 9900 } 9901 9902 private bool firstShow = true; 9903 9904 @scriptable 9905 override void show() { 9906 bool rd = false; 9907 if(firstShow) { 9908 firstShow = false; 9909 queueRecomputeChildLayout(); 9910 // unless the programmer already called focus on something, pick something ourselves 9911 auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus? 9912 if(f) 9913 f.focus(); 9914 redraw(); 9915 } 9916 win.show(); 9917 super.show(); 9918 } 9919 @scriptable 9920 override void hide() { 9921 win.hide(); 9922 super.hide(); 9923 } 9924 9925 static Widget getFirstFocusable(Widget start) { 9926 if(start is null) 9927 return null; 9928 9929 foreach(widget; &start.focusableWidgets) { 9930 return widget; 9931 } 9932 9933 return null; 9934 } 9935 9936 static Widget getLastFocusable(Widget start) { 9937 if(start is null) 9938 return null; 9939 9940 Widget last; 9941 foreach(widget; &start.focusableWidgets) { 9942 last = widget; 9943 } 9944 9945 return last; 9946 } 9947 9948 9949 mixin Emits!ClosingEvent; 9950 mixin Emits!ClosedEvent; 9951 } 9952 9953 /++ 9954 History: 9955 Added January 12, 2022 9956 9957 Made `final` on January 3, 2025 9958 +/ 9959 final class DpiChangedEvent : Event { 9960 enum EventString = "dpichanged"; 9961 9962 this(Widget target) { 9963 super(EventString, target); 9964 } 9965 } 9966 9967 debug private class DevToolWindow : Window { 9968 Window p; 9969 9970 TextEdit parentList; 9971 TextEdit logWindow; 9972 TextLabel clickX, clickY; 9973 9974 this(Window p) { 9975 this.p = p; 9976 super(400, 300, "Developer Toolbox"); 9977 9978 logWindow = new TextEdit(this); 9979 parentList = new TextEdit(this); 9980 9981 auto hl = new HorizontalLayout(this); 9982 clickX = new TextLabel("", TextAlignment.Right, hl); 9983 clickY = new TextLabel("", TextAlignment.Right, hl); 9984 9985 parentListeners ~= p.addEventListener("*", (Event ev) { 9986 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 9987 }); 9988 9989 parentListeners ~= p.addEventListener((ClickEvent ev) { 9990 auto s = ev.srcElement; 9991 9992 string list; 9993 9994 void addInfo(Widget s) { 9995 list ~= s.toString(); 9996 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 9997 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 9998 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 9999 list ~= "\n\theight: " ~ toInternal!string(s.height); 10000 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 10001 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 10002 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 10003 list ~= "\n\twidth: " ~ toInternal!string(s.width); 10004 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 10005 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 10006 } 10007 10008 addInfo(s); 10009 10010 s = s.parent; 10011 while(s) { 10012 list ~= "\n"; 10013 addInfo(s); 10014 s = s.parent; 10015 } 10016 parentList.content = list; 10017 10018 clickX.label = toInternal!string(ev.clientX); 10019 clickY.label = toInternal!string(ev.clientY); 10020 }); 10021 } 10022 10023 EventListener[] parentListeners; 10024 10025 override void close() { 10026 assert(p !is null); 10027 foreach(p; parentListeners) 10028 p.disconnect(); 10029 parentListeners = null; 10030 p.devTools = null; 10031 p = null; 10032 super.close(); 10033 } 10034 10035 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10036 if(ev.key == Key.F12) { 10037 this.close(); 10038 if(p) 10039 p.devTools = null; 10040 } else { 10041 super.defaultEventHandler_keydown(ev); 10042 } 10043 } 10044 10045 void log(T...)(T t) { 10046 string str; 10047 import std.conv; 10048 foreach(i; t) 10049 str ~= to!string(i); 10050 str ~= "\n"; 10051 logWindow.addText(str); 10052 logWindow.scrollToBottom(); 10053 10054 //version(custom_widgets) 10055 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 10056 } 10057 } 10058 10059 /++ 10060 A dialog is a transient window that intends to get information from 10061 the user before being dismissed. 10062 +/ 10063 class Dialog : Window { 10064 /// 10065 this(Window parent, int width, int height, string title = null) { 10066 super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win); 10067 10068 // this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 10069 } 10070 10071 /// 10072 this(Window parent, string title, int width, int height) { 10073 this(parent, width, height, title); 10074 } 10075 10076 deprecated("Pass an explicit parent window, even if it is `null`") 10077 this(int width, int height, string title = null) { 10078 this(null, width, height, title); 10079 } 10080 10081 /// 10082 void OK() { 10083 10084 } 10085 10086 /// 10087 void Cancel() { 10088 this.close(); 10089 } 10090 } 10091 10092 /++ 10093 A custom widget similar to the HTML5 <details> tag. 10094 +/ 10095 version(none) 10096 class DetailsView : Widget { 10097 10098 } 10099 10100 // FIXME: maybe i should expose the other list views Windows offers too 10101 10102 /++ 10103 A TableView is a widget made to display a table of data strings. 10104 10105 10106 Future_Directions: 10107 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 10108 10109 I will add a selection changed event at some point, as well as item clicked events. 10110 History: 10111 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 10112 See_Also: 10113 [ListWidget] which displays a list of strings without additional columns. 10114 +/ 10115 class TableView : Widget { 10116 /++ 10117 10118 +/ 10119 this(Widget parent) { 10120 super(parent); 10121 10122 version(win32_widgets) { 10123 // LVS_EX_LABELTIP might be worth too 10124 // LVS_OWNERDRAWFIXED 10125 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//, LVS_EX_TRACKSELECT); // ex style for for LVN_HOTTRACK 10126 } else version(custom_widgets) { 10127 auto smw = new ScrollMessageWidget(this); 10128 smw.addDefaultKeyboardListeners(); 10129 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 10130 tvwi = new TableViewWidgetInner(this, smw); 10131 } 10132 } 10133 10134 // FIXME: auto-size columns on double click of header thing like in Windows 10135 // it need only make the currently displayed things fit well. 10136 10137 10138 private ColumnInfo[] columns; 10139 private int itemCount; 10140 10141 version(custom_widgets) private { 10142 TableViewWidgetInner tvwi; 10143 } 10144 10145 /// Passed to [setColumnInfo] 10146 static struct ColumnInfo { 10147 const(char)[] name; /// the name displayed in the header 10148 /++ 10149 The default width, in pixels. As a special case, you can set this to -1 10150 if you want the system to try to automatically size the width to fit visible 10151 content. If it can't, it will try to pick a sensible default size. 10152 10153 Any other negative value is not allowed and may lead to unpredictable results. 10154 10155 History: 10156 The -1 behavior was specified on December 3, 2021. It actually worked before 10157 anyway on Win32 but now it is a formal feature with partial Linux support. 10158 10159 Bugs: 10160 It doesn't actually attempt to calculate a best-fit width on Linux as of 10161 December 3, 2021. I do plan to fix this in the future, but Windows is the 10162 priority right now. At least it doesn't break things when you use it now. 10163 +/ 10164 int width; 10165 10166 /++ 10167 Alignment of the text in the cell. Applies to the header as well as all data in this 10168 column. 10169 10170 Bugs: 10171 On Windows, the first column ignores this member and is always left aligned. 10172 You can work around this by inserting a dummy first column with width = 0 10173 then putting your actual data in the second column, which does respect the 10174 alignment. 10175 10176 This is a quirk of the operating system's implementation going back a very 10177 long time and is unlikely to ever be fixed. 10178 +/ 10179 TextAlignment alignment; 10180 10181 /++ 10182 After all the pixel widths have been assigned, any left over 10183 space is divided up among all columns and distributed to according 10184 to the widthPercent field. 10185 10186 10187 For example, if you have two fields, both with width 50 and one with 10188 widthPercent of 25 and the other with widthPercent of 75, and the 10189 container is 200 pixels wide, first both get their width of 50. 10190 then the 100 remaining pixels are split up, so the one gets a total 10191 of 75 pixels and the other gets a total of 125. 10192 10193 This is automatically applied as the window is resized. 10194 10195 If there is not enough space - that is, when a horizontal scrollbar 10196 needs to appear - there are 0 pixels divided up, and thus everyone 10197 gets 0. This can cause a column to shrink out of proportion when 10198 passing the scroll threshold. 10199 10200 It is important to still set a fixed width (that is, to populate the 10201 `width` field) even if you use the percents because that will be the 10202 default minimum in the event of a scroll bar appearing. 10203 10204 The percents total in the column can never exceed 100 or be less than 0. 10205 Doing this will trigger an assert error. 10206 10207 Implementation note: 10208 10209 Please note that percentages are only recalculated 1) upon original 10210 construction and 2) upon resizing the control. If the user adjusts the 10211 width of a column, the percentage items will not be updated. 10212 10213 On the other hand, if the user adjusts the width of a percentage column 10214 then resizes the window, it is recalculated, meaning their hand adjustment 10215 is discarded. This specific behavior may change in the future as it is 10216 arguably a bug, but I'm not certain yet. 10217 10218 History: 10219 Added November 10, 2021 (dub v10.4) 10220 +/ 10221 int widthPercent; 10222 10223 10224 private int calculatedWidth; 10225 } 10226 /++ 10227 Sets the number of columns along with information about the headers. 10228 10229 Please note: on Windows, the first column ignores your alignment preference 10230 and is always left aligned. 10231 +/ 10232 void setColumnInfo(ColumnInfo[] columns...) { 10233 10234 foreach(ref c; columns) { 10235 c.name = c.name.idup; 10236 } 10237 this.columns = columns.dup; 10238 10239 updateCalculatedWidth(false); 10240 10241 version(custom_widgets) { 10242 tvwi.header.updateHeaders(); 10243 tvwi.updateScrolls(); 10244 } else version(win32_widgets) 10245 foreach(i, column; this.columns) { 10246 LVCOLUMN lvColumn; 10247 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 10248 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 10249 10250 auto bfr = WCharzBuffer(column.name); 10251 lvColumn.pszText = bfr.ptr; 10252 10253 if(column.alignment & TextAlignment.Center) 10254 lvColumn.fmt = LVCFMT_CENTER; 10255 else if(column.alignment & TextAlignment.Right) 10256 lvColumn.fmt = LVCFMT_RIGHT; 10257 else 10258 lvColumn.fmt = LVCFMT_LEFT; 10259 10260 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 10261 throw new WindowsApiException("Insert Column Fail", GetLastError()); 10262 } 10263 } 10264 10265 version(custom_widgets) 10266 private int getColumnSizeForContent(size_t columnIndex) { 10267 // FIXME: idk where the problem is but with a 2x scale the horizontal scroll is insuffiicent. i think the SMW is doing it wrong. 10268 // might also want a user-defined max size too 10269 int padding = scaleWithDpi(6); 10270 int m = this.defaultTextWidth(this.columns[columnIndex].name) + padding; 10271 10272 if(getData !is null) 10273 foreach(row; 0 .. itemCount) 10274 getData(row, cast(int) columnIndex, (txt) { 10275 m = mymax(m, this.defaultTextWidth(txt) + padding); 10276 }); 10277 10278 if(m < 32) 10279 m = 32; 10280 10281 return m; 10282 } 10283 10284 /++ 10285 History: 10286 Added February 26, 2025 10287 +/ 10288 void autoSizeColumnsToContent() { 10289 version(custom_widgets) { 10290 foreach(idx, ref c; columns) { 10291 c.width = getColumnSizeForContent(idx); 10292 } 10293 updateCalculatedWidth(false); 10294 tvwi.updateScrolls(); 10295 } else version(win32_widgets) { 10296 foreach(i, c; columns) 10297 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, LVSCW_AUTOSIZE); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 10298 } 10299 } 10300 10301 /++ 10302 History: 10303 Added March 1, 2025 10304 +/ 10305 bool supportsPerCellAlignment() { 10306 version(custom_widgets) 10307 return true; 10308 else version(win32_widgets) 10309 return false; 10310 return false; 10311 } 10312 10313 private int getActualSetSize(size_t i, bool askWindows) { 10314 version(win32_widgets) 10315 if(askWindows) 10316 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 10317 auto w = columns[i].width; 10318 if(w == -1) 10319 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 10320 return w; 10321 } 10322 10323 private void updateCalculatedWidth(bool informWindows) { 10324 int padding; 10325 version(win32_widgets) 10326 padding = 4; 10327 int remaining = this.width; 10328 foreach(i, column; columns) 10329 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 10330 remaining -= padding; 10331 if(remaining < 0) 10332 remaining = 0; 10333 10334 int percentTotal; 10335 foreach(i, ref column; columns) { 10336 percentTotal += column.widthPercent; 10337 10338 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 10339 10340 column.calculatedWidth = c; 10341 10342 version(win32_widgets) 10343 if(informWindows) 10344 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 10345 } 10346 10347 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 10348 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)."); 10349 10350 10351 } 10352 10353 override void registerMovement() { 10354 super.registerMovement(); 10355 10356 updateCalculatedWidth(true); 10357 } 10358 10359 /++ 10360 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. 10361 +/ 10362 void setItemCount(int count) { 10363 this.itemCount = count; 10364 version(custom_widgets) { 10365 tvwi.updateScrolls(); 10366 redraw(); 10367 } else version(win32_widgets) { 10368 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 10369 } 10370 } 10371 10372 /++ 10373 Clears all items; 10374 +/ 10375 void clear() { 10376 this.itemCount = 0; 10377 this.columns = null; 10378 version(custom_widgets) { 10379 tvwi.header.updateHeaders(); 10380 tvwi.updateScrolls(); 10381 redraw(); 10382 } else version(win32_widgets) { 10383 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 10384 } 10385 } 10386 10387 /+ 10388 version(win32_widgets) 10389 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 10390 auto itemId = dis.itemID; 10391 auto hdc = dis.hDC; 10392 auto rect = dis.rcItem; 10393 switch(dis.itemAction) { 10394 case ODA_DRAWENTIRE: 10395 10396 // FIXME: do other items 10397 // FIXME: do the focus rectangle i guess 10398 // FIXME: alignment 10399 // FIXME: column width 10400 // FIXME: padding left 10401 // FIXME: check dpi scaling 10402 // FIXME: don't owner draw unless it is necessary. 10403 10404 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 10405 RECT itemRect; 10406 itemRect.top = 1; // subitem idx, 1-based 10407 itemRect.left = LVIR_BOUNDS; 10408 10409 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 10410 itemRect.left += padding; 10411 10412 getData(itemId, 0, (in char[] data) { 10413 auto wdata = WCharzBuffer(data); 10414 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 10415 10416 }); 10417 goto case; 10418 case ODA_FOCUS: 10419 if(dis.itemState & ODS_FOCUS) 10420 DrawFocusRect(hdc, &rect); 10421 break; 10422 case ODA_SELECT: 10423 // itemState & ODS_SELECTED 10424 break; 10425 default: 10426 } 10427 return 1; 10428 } 10429 +/ 10430 10431 version(win32_widgets) { 10432 CellStyle last; 10433 COLORREF defaultColor; 10434 COLORREF defaultBackground; 10435 } 10436 10437 version(win32_widgets) 10438 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 10439 switch(code) { 10440 case NM_CUSTOMDRAW: 10441 auto s = cast(NMLVCUSTOMDRAW*) hdr; 10442 switch(s.nmcd.dwDrawStage) { 10443 case CDDS_PREPAINT: 10444 if(getCellStyle is null) 10445 return 0; 10446 10447 mustReturn = true; 10448 return CDRF_NOTIFYITEMDRAW; 10449 case CDDS_ITEMPREPAINT: 10450 mustReturn = true; 10451 return CDRF_NOTIFYSUBITEMDRAW; 10452 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 10453 mustReturn = true; 10454 10455 if(getCellStyle is null) // this SHOULD never happen... 10456 return 0; 10457 10458 if(s.iSubItem == 0) { 10459 // Windows resets it per row so we'll use item 0 as a chance 10460 // to capture these for later 10461 defaultColor = s.clrText; 10462 defaultBackground = s.clrTextBk; 10463 } 10464 10465 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 10466 // if no special style and no reset needed... 10467 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 10468 return 0; // allow default processing to continue 10469 10470 last = style; 10471 10472 // might still need to reset or use the preference. 10473 10474 if(style.flags & CellStyle.Flags.textColorSet) 10475 s.clrText = style.textColor.asWindowsColorRef; 10476 else 10477 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 10478 if(style.flags & CellStyle.Flags.backgroundColorSet) 10479 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 10480 else 10481 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 10482 10483 return CDRF_NEWFONT; 10484 default: 10485 return 0; 10486 10487 } 10488 case NM_RETURN: // no need since i subclass keydown 10489 break; 10490 case LVN_COLUMNCLICK: 10491 auto info = cast(LPNMLISTVIEW) hdr; 10492 this.emit!HeaderClickedEvent(info.iSubItem); 10493 break; 10494 case (LVN_FIRST-21) /* LVN_HOTTRACK */: 10495 // requires LVS_EX_TRACKSELECT 10496 // sdpyPrintDebugString("here"); 10497 mustReturn = 1; // override Windows' auto selection 10498 break; 10499 case NM_CLICK: 10500 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10501 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.left, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), false); 10502 break; 10503 case NM_DBLCLK: 10504 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10505 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.left, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), true); 10506 break; 10507 case NM_RCLICK: 10508 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10509 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.right, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), false); 10510 break; 10511 case NM_RDBLCLK: 10512 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10513 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.right, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), true); 10514 break; 10515 case LVN_GETDISPINFO: 10516 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 10517 if(info.item.mask & LVIF_TEXT) { 10518 if(getData) { 10519 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 10520 auto bfr = WCharzBuffer(dataReceived); 10521 auto len = info.item.cchTextMax; 10522 if(bfr.length < len) 10523 len = cast(typeof(len)) bfr.length; 10524 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 10525 info.item.pszText[len] = 0; 10526 }); 10527 } else { 10528 info.item.pszText[0] = 0; 10529 } 10530 //info.item.iItem 10531 //if(info.item.iSubItem) 10532 } 10533 break; 10534 default: 10535 } 10536 return 0; 10537 } 10538 10539 // FIXME: this throws off mouse calculations, it should only happen when we're at the top level or something idk 10540 override bool encapsulatedChildren() { 10541 return true; 10542 } 10543 10544 /++ 10545 Informs the control that content has changed. 10546 10547 History: 10548 Added November 10, 2021 (dub v10.4) 10549 +/ 10550 void update() { 10551 version(custom_widgets) 10552 redraw(); 10553 else { 10554 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 10555 UpdateWindow(hwnd); 10556 } 10557 10558 10559 } 10560 10561 /++ 10562 Called by the system to request the text content of an individual cell. You 10563 should pass the text into the provided `sink` delegate. This function will be 10564 called for each visible cell as-needed when drawing. 10565 +/ 10566 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 10567 10568 /++ 10569 Available per-cell style customization options. Use one of the constructors 10570 provided to set the values conveniently, or default construct it and set individual 10571 values yourself. Just remember to set the `flags` so your values are actually used. 10572 If the flag isn't set, the field is ignored and the system default is used instead. 10573 10574 This is returned by the [getCellStyle] delegate. 10575 10576 Examples: 10577 --- 10578 // assumes you have a variables called `my_data` which is an array of arrays of numbers 10579 auto table = new TableView(window); 10580 // snip: you would set up columns here 10581 10582 // this is how you provide data to the table view class 10583 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 10584 import std.conv; 10585 sink(to!string(my_data[row][column])); 10586 }; 10587 10588 // and this is how you customize the colors 10589 table.getCellStyle = delegate(int row, int column) { 10590 return (my_data[row][column] < 0) ? 10591 TableView.CellStyle(Color.red); // make negative numbers red 10592 : TableView.CellStyle.init; // leave the rest alone 10593 }; 10594 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 10595 --- 10596 10597 History: 10598 Added November 27, 2021 (dub v10.4) 10599 +/ 10600 struct CellStyle { 10601 /// 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. 10602 this(Color textColor) { 10603 this.textColor = textColor; 10604 this.flags |= Flags.textColorSet; 10605 } 10606 /// Sets a custom text and background color. 10607 this(Color textColor, Color backgroundColor) { 10608 this.textColor = textColor; 10609 this.backgroundColor = backgroundColor; 10610 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 10611 } 10612 /++ 10613 Alignment is only supported on some platforms. 10614 +/ 10615 this(TextAlignment alignment) { 10616 this.alignment = alignment; 10617 this.flags |= Flags.alignmentSet; 10618 } 10619 /// ditto 10620 this(TextAlignment alignment, Color textColor) { 10621 this.alignment = alignment; 10622 this.textColor = textColor; 10623 this.flags |= Flags.alignmentSet | Flags.textColorSet; 10624 } 10625 /// ditto 10626 this(TextAlignment alignment, Color textColor, Color backgroundColor) { 10627 this.alignment = alignment; 10628 this.textColor = textColor; 10629 this.backgroundColor = backgroundColor; 10630 this.flags |= Flags.alignmentSet | Flags.textColorSet | Flags.backgroundColorSet; 10631 } 10632 10633 TextAlignment alignment; 10634 Color textColor; 10635 Color backgroundColor; 10636 int flags; /// bitmask of [Flags] 10637 /// available options to combine into [flags] 10638 enum Flags { 10639 textColorSet = 1 << 0, 10640 backgroundColorSet = 1 << 1, 10641 alignmentSet = 1 << 2, 10642 } 10643 } 10644 /++ 10645 Companion delegate to [getData] that allows you to custom style each 10646 cell of the table. 10647 10648 Returns: 10649 A [CellStyle] structure that describes the desired style for the 10650 given cell. `return CellStyle.init` if you want the default style. 10651 10652 History: 10653 Added November 27, 2021 (dub v10.4) 10654 +/ 10655 CellStyle delegate(int row, int column) getCellStyle; 10656 10657 // i want to be able to do things like draw little colored things to show red for negative numbers 10658 // or background color indicators or even in-cell charts 10659 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 10660 10661 /++ 10662 When the user clicks on a header, this event is emitted. It has a member to identify which header (by index) was clicked. 10663 +/ 10664 mixin Emits!HeaderClickedEvent; 10665 10666 /++ 10667 History: 10668 Added March 2, 2025 10669 +/ 10670 mixin Emits!CellClickedEvent; 10671 } 10672 10673 /++ 10674 This is emitted by the [TableView] when a user clicks on a column header. 10675 10676 Its member `columnIndex` has the zero-based index of the column that was clicked. 10677 10678 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 10679 10680 History: 10681 Added November 27, 2021 (dub v10.4) 10682 10683 Made `final` on January 3, 2025 10684 +/ 10685 final class HeaderClickedEvent : Event { 10686 enum EventString = "HeaderClicked"; 10687 this(Widget target, int columnIndex) { 10688 this.columnIndex = columnIndex; 10689 super(EventString, target); 10690 } 10691 10692 /// The index of the column 10693 int columnIndex; 10694 10695 /// 10696 override @property int intValue() { 10697 return columnIndex; 10698 } 10699 } 10700 10701 /++ 10702 History: 10703 Added March 2, 2025 10704 +/ 10705 final class CellClickedEvent : MouseEventBase { 10706 enum EventString = "CellClicked"; 10707 this(Widget target, int rowIndex, int columnIndex, MouseButton button, MouseButtonLinear mouseButtonLinear, int x, int y, bool altKey, bool ctrlKey, bool shiftKey, bool isDoubleClick) { 10708 this.rowIndex = rowIndex; 10709 this.columnIndex = columnIndex; 10710 this.button = button; 10711 this.buttonLinear = mouseButtonLinear; 10712 this.isDoubleClick = isDoubleClick; 10713 this.clientX = x; 10714 this.clientY = y; 10715 10716 this.altKey = altKey; 10717 this.ctrlKey = ctrlKey; 10718 this.shiftKey = shiftKey; 10719 10720 // import std.stdio; std.stdio.writeln(rowIndex, "x", columnIndex, " @ ", x, ",", y, " ", button, " ", isDoubleClick, " ", altKey, " ", ctrlKey, " ", shiftKey); 10721 10722 // FIXME: x, y, state, altButton etc? 10723 super(EventString, target); 10724 } 10725 10726 /++ 10727 See also: [button] inherited from the base class. 10728 10729 clientX and clientY are irrespective of scrolling - FIXME is that sane? 10730 +/ 10731 int columnIndex; 10732 10733 /// ditto 10734 int rowIndex; 10735 10736 /// ditto 10737 bool isDoubleClick; 10738 10739 /+ 10740 // i could do intValue as a linear index if we know the width 10741 // and a stringValue with the string in the cell. but idk if worth. 10742 override @property int intValue() { 10743 return columnIndex; 10744 } 10745 +/ 10746 10747 } 10748 10749 version(custom_widgets) 10750 private class TableViewWidgetInner : Widget { 10751 10752 // wrap this thing in a ScrollMessageWidget 10753 10754 TableView tvw; 10755 ScrollMessageWidget smw; 10756 HeaderWidget header; 10757 10758 this(TableView tvw, ScrollMessageWidget smw) { 10759 this.tvw = tvw; 10760 this.smw = smw; 10761 super(smw); 10762 10763 this.tabStop = true; 10764 10765 header = new HeaderWidget(this, smw.getHeader()); 10766 10767 smw.addEventListener("scroll", () { 10768 this.redraw(); 10769 header.redraw(); 10770 }); 10771 10772 10773 // I need headers outside the scroll area but rendered on the same line as the up arrow 10774 // FIXME: add a fixed header to the SMW 10775 } 10776 10777 enum padding = 3; 10778 10779 void updateScrolls() { 10780 int w; 10781 foreach(idx, column; tvw.columns) { 10782 w += column.calculatedWidth; 10783 } 10784 smw.setTotalArea(w, tvw.itemCount); 10785 columnsWidth = w; 10786 } 10787 10788 private int columnsWidth; 10789 10790 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 10791 10792 override void registerMovement() { 10793 super.registerMovement(); 10794 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 10795 smw.setViewableArea(this.width, this.height / lh); 10796 } 10797 10798 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 10799 int x; 10800 int y; 10801 10802 int row = smw.position.y; 10803 10804 foreach(lol; 0 .. this.height / lh) { 10805 if(row >= tvw.itemCount) 10806 break; 10807 x = 0; 10808 foreach(columnNumber, column; tvw.columns) { 10809 auto x2 = x + column.calculatedWidth; 10810 auto smwx = smw.position.x; 10811 10812 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 10813 auto startX = x; 10814 auto endX = x + column.calculatedWidth; 10815 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 10816 case TextAlignment.Left: startX += padding; break; 10817 case TextAlignment.Center: startX += padding; endX -= padding; break; 10818 case TextAlignment.Right: endX -= padding; break; 10819 default: /* broken */ break; 10820 } 10821 if(column.width != 0) // no point drawing an invisible column 10822 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 10823 auto endClip = endX - smw.position.x; 10824 if(endClip > this.width - padding) 10825 endClip = this.width - padding; 10826 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endClip, y + lh))); 10827 10828 void dotext(WidgetPainter painter, TextAlignment alignment) { 10829 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x - padding, y + lh), alignment); 10830 } 10831 10832 if(tvw.getCellStyle !is null) { 10833 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 10834 10835 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 10836 auto tempPainter = painter; 10837 tempPainter.fillColor = style.backgroundColor; 10838 tempPainter.outlineColor = style.backgroundColor; 10839 10840 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 10841 Point(endX - smw.position.x, y + lh)); 10842 } 10843 auto tempPainter = painter; 10844 if(style.flags & TableView.CellStyle.Flags.textColorSet) 10845 tempPainter.outlineColor = style.textColor; 10846 10847 auto alignment = column.alignment; 10848 if(style.flags & TableView.CellStyle.Flags.alignmentSet) 10849 alignment = style.alignment; 10850 dotext(tempPainter, alignment); 10851 } else { 10852 dotext(painter, column.alignment); 10853 } 10854 }); 10855 } 10856 10857 x += column.calculatedWidth; 10858 } 10859 row++; 10860 y += lh; 10861 } 10862 return bounds; 10863 } 10864 10865 static class Style : Widget.Style { 10866 override WidgetBackground background() { 10867 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 10868 } 10869 } 10870 mixin OverrideStyle!Style; 10871 10872 private static class HeaderWidget : Widget { 10873 /+ 10874 maybe i should do a splitter thing on top of the other widgets 10875 so the splitter itself isn't really drawn but still replies to mouse events? 10876 +/ 10877 this(TableViewWidgetInner tvw, Widget parent) { 10878 super(parent); 10879 this.tvw = tvw; 10880 10881 this.remainder = new Button("", this); 10882 10883 this.addEventListener((scope ClickEvent ev) { 10884 int header = -1; 10885 foreach(idx, child; this.children[1 .. $]) { 10886 if(child is ev.target) { 10887 header = cast(int) idx; 10888 break; 10889 } 10890 } 10891 10892 if(header != -1) { 10893 auto hce = new HeaderClickedEvent(tvw.tvw, header); 10894 hce.dispatch(); 10895 } 10896 10897 }); 10898 } 10899 10900 override int minHeight() { 10901 return defaultLineHeight + 4; // same as Button 10902 } 10903 10904 void updateHeaders() { 10905 foreach(child; children[1 .. $]) 10906 child.removeWidget(); 10907 10908 foreach(column; tvw.tvw.columns) { 10909 // the cast is ok because I dup it above, just the type is never changed. 10910 // all this is private so it should never get messed up. 10911 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 10912 } 10913 } 10914 10915 Button remainder; 10916 TableViewWidgetInner tvw; 10917 10918 override void recomputeChildLayout() { 10919 registerMovement(); 10920 int pos; 10921 foreach(idx, child; children[1 .. $]) { 10922 if(idx >= tvw.tvw.columns.length) 10923 continue; 10924 child.x = pos; 10925 child.y = 0; 10926 child.width = tvw.tvw.columns[idx].calculatedWidth; 10927 child.height = scaleWithDpi(16);// this.height; 10928 pos += child.width; 10929 10930 child.recomputeChildLayout(); 10931 } 10932 10933 if(remainder is null) 10934 return; 10935 10936 remainder.x = pos; 10937 remainder.y = 0; 10938 if(pos < this.width) 10939 remainder.width = this.width - pos;// + 4; 10940 else 10941 remainder.width = 0; 10942 remainder.height = scaleWithDpi(16); 10943 10944 remainder.recomputeChildLayout(); 10945 } 10946 10947 // for the scrollable children mixin 10948 Point scrollOrigin() { 10949 return Point(tvw.smw.position.x, 0); 10950 } 10951 void paintFrameAndBackground(WidgetPainter painter) { } 10952 10953 // for mouse event dispatching 10954 override protected void addScrollPosition(ref int x, ref int y) { 10955 x += scrollOrigin.x; 10956 y += scrollOrigin.y; 10957 } 10958 10959 mixin ScrollableChildren; 10960 } 10961 10962 private void emitCellClickedEvent(scope MouseEventBase event, bool isDoubleClick) { 10963 int mx = event.clientX + smw.position.x; 10964 int my = event.clientY; 10965 10966 Widget par = this; 10967 while(par && !par.encapsulatedChildren) { 10968 my -= par.y; // to undo the encapsulatedChildren adjustClientCoordinates effect 10969 par = par.parent; 10970 } 10971 if(par is null) 10972 my = event.clientY; // encapsulatedChildren not present? 10973 10974 int row = my / lh + smw.position.y; // scrolling here is done per-item, not per pixel 10975 if(row > tvw.itemCount) 10976 row = -1; 10977 10978 int column = -1; 10979 if(row != -1) { 10980 int pos; 10981 foreach(idx, col; tvw.columns) { 10982 pos += col.calculatedWidth; 10983 if(mx < pos) { 10984 column = cast(int) idx; 10985 break; 10986 } 10987 } 10988 } 10989 10990 // wtf are these casts about? 10991 tvw.emit!CellClickedEvent(row, column, cast(MouseButton) event.button, cast(MouseButtonLinear) event.buttonLinear, event.clientX, event.clientY, event.altKey, event.ctrlKey, event.shiftKey, isDoubleClick); 10992 } 10993 10994 override void defaultEventHandler_click(scope ClickEvent ce) { 10995 // FIXME: should i filter mouse wheel events? Windows doesn't send them but i can. 10996 emitCellClickedEvent(ce, false); 10997 } 10998 10999 override void defaultEventHandler_dblclick(scope DoubleClickEvent ce) { 11000 emitCellClickedEvent(ce, true); 11001 } 11002 } 11003 11004 /+ 11005 11006 // given struct / array / number / string / etc, make it viewable and editable 11007 class DataViewerWidget : Widget { 11008 11009 } 11010 +/ 11011 11012 /++ 11013 A line edit box with an associated label. 11014 11015 History: 11016 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 11017 11018 ``` 11019 Old: ________ 11020 11021 New: 11022 ____________ 11023 ``` 11024 11025 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 11026 11027 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 11028 horizontal label but left aligned. You may also consider a [GridLayout]. 11029 +/ 11030 alias LabeledLineEdit = Labeled!LineEdit; 11031 11032 private int widthThatWouldFitChildLabels(Widget w) { 11033 if(w is null) 11034 return 0; 11035 11036 int max; 11037 11038 if(auto label = cast(TextLabel) w) { 11039 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 11040 } else { 11041 foreach(child; w.children) { 11042 max = mymax(max, widthThatWouldFitChildLabels(child)); 11043 } 11044 } 11045 11046 return max; 11047 } 11048 11049 /++ 11050 History: 11051 Added May 19, 2021 11052 +/ 11053 class Labeled(T) : Widget { 11054 /// 11055 this(string label, Widget parent) { 11056 super(parent); 11057 initialize!VerticalLayout(label, TextAlignment.Left, parent); 11058 } 11059 11060 /++ 11061 History: 11062 The alignment parameter was added May 17, 2021 11063 +/ 11064 this(string label, TextAlignment alignment, Widget parent) { 11065 super(parent); 11066 initialize!HorizontalLayout(label, alignment, parent); 11067 } 11068 11069 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 11070 tabStop = false; 11071 horizontal = is(L == HorizontalLayout); 11072 auto hl = new L(this); 11073 if(horizontal) { 11074 static class SpecialTextLabel : TextLabel { 11075 Widget outerParent; 11076 11077 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 11078 this.outerParent = outerParent; 11079 super(label, alignment, parent); 11080 } 11081 11082 override int flexBasisWidth() { 11083 return widthThatWouldFitChildLabels(outerParent); 11084 } 11085 /+ 11086 override int widthShrinkiness() { return 0; } 11087 override int widthStretchiness() { return 1; } 11088 +/ 11089 11090 override int paddingRight() { return 6; } 11091 override int paddingLeft() { return 9; } 11092 11093 override int paddingTop() { return 3; } 11094 } 11095 this.label = new SpecialTextLabel(label, alignment, parent, hl); 11096 } else 11097 this.label = new TextLabel(label, alignment, hl); 11098 this.lineEdit = new T(hl); 11099 11100 this.label.labelFor = this.lineEdit; 11101 } 11102 11103 private bool horizontal; 11104 11105 TextLabel label; /// 11106 T lineEdit; /// 11107 11108 override int flexBasisWidth() { return 250; } 11109 override int widthShrinkiness() { return 1; } 11110 11111 override int minHeight() { 11112 return this.children[0].minHeight; 11113 } 11114 override int maxHeight() { return minHeight(); } 11115 override int marginTop() { return 4; } 11116 override int marginBottom() { return 4; } 11117 11118 // FIXME: i should prolly call it value as well as content tbh 11119 11120 /// 11121 @property string content() { 11122 return lineEdit.content; 11123 } 11124 /// 11125 @property void content(string c) { 11126 return lineEdit.content(c); 11127 } 11128 11129 /// 11130 void selectAll() { 11131 lineEdit.selectAll(); 11132 } 11133 11134 override void focus() { 11135 lineEdit.focus(); 11136 } 11137 } 11138 11139 /++ 11140 A labeled password edit. 11141 11142 History: 11143 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 11144 11145 The default parameters for the constructors were also removed on May 19, 2021 11146 +/ 11147 alias LabeledPasswordEdit = Labeled!PasswordEdit; 11148 11149 private string toMenuLabel(string s) { 11150 string n; 11151 n.reserve(s.length); 11152 foreach(c; s) 11153 if(c == '_') 11154 n ~= ' '; 11155 else 11156 n ~= c; 11157 return n; 11158 } 11159 11160 private void autoExceptionHandler(Exception e) { 11161 messageBox(e.msg); 11162 } 11163 11164 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) { 11165 makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))(); 11166 } 11167 11168 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) { 11169 static if(is(T : void delegate())) { 11170 return () { 11171 try 11172 t(); 11173 catch(Exception e) 11174 autoExceptionHandler(e); 11175 }; 11176 } else static if(is(typeof(fn) Params == __parameters)) { 11177 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 11178 return () { 11179 void onOK(string s) { 11180 member = s; 11181 try 11182 t(Params[0](s)); 11183 catch(Exception e) 11184 autoExceptionHandler(e); 11185 } 11186 11187 if( 11188 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 11189 || type == FileDialogType.Save) 11190 { 11191 getSaveFileName(window, &onOK, member, filters, null); 11192 } else 11193 getOpenFileName(window, &onOK, member, filters, null); 11194 }; 11195 } else { 11196 struct S { 11197 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 11198 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 11199 } else mixin(q{ 11200 static foreach(idx, ignore; Params) { 11201 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 11202 } 11203 }); 11204 } 11205 return () { 11206 dialog(window, (S s) { 11207 try { 11208 static if(is(typeof(t) Ret == return)) { 11209 static if(is(Ret == void)) { 11210 t(s.tupleof); 11211 } else { 11212 auto ret = t(s.tupleof); 11213 import std.conv; 11214 messageBox(to!string(ret), "Returned Value"); 11215 } 11216 } 11217 } catch(Exception e) 11218 autoExceptionHandler(e); 11219 }, null, __traits(identifier, fn)); 11220 }; 11221 } 11222 } 11223 } 11224 11225 private template hasAnyRelevantAnnotations(a...) { 11226 bool helper() { 11227 bool any; 11228 foreach(attr; a) { 11229 static if(is(typeof(attr) == .menu)) 11230 any = true; 11231 else static if(is(typeof(attr) == .toolbar)) 11232 any = true; 11233 else static if(is(attr == .separator)) 11234 any = true; 11235 else static if(is(typeof(attr) == .accelerator)) 11236 any = true; 11237 else static if(is(typeof(attr) == .hotkey)) 11238 any = true; 11239 else static if(is(typeof(attr) == .icon)) 11240 any = true; 11241 else static if(is(typeof(attr) == .label)) 11242 any = true; 11243 else static if(is(typeof(attr) == .tip)) 11244 any = true; 11245 } 11246 return any; 11247 } 11248 11249 enum bool hasAnyRelevantAnnotations = helper(); 11250 } 11251 11252 /++ 11253 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. 11254 +/ 11255 class MainWindow : Window { 11256 /// 11257 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 11258 super(initialWidth, initialHeight, title); 11259 11260 _clientArea = new ClientAreaWidget(); 11261 _clientArea.x = 0; 11262 _clientArea.y = 0; 11263 _clientArea.width = this.width; 11264 _clientArea.height = this.height; 11265 _clientArea.tabStop = false; 11266 11267 super.addChild(_clientArea); 11268 11269 statusBar = new StatusBar(this); 11270 } 11271 11272 /++ 11273 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). 11274 11275 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')`. 11276 11277 You can also use `@separator` to put a separating line in the menu before the function. 11278 11279 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. 11280 11281 Let's look at a complete example: 11282 11283 --- 11284 import arsd.minigui; 11285 11286 void main() { 11287 auto window = new MainWindow(); 11288 11289 // we can add widgets before or after setting the menu, either way is fine. 11290 // i'll do it before here so the local variables are available to the commands. 11291 11292 auto textEdit = new TextEdit(window); 11293 11294 // Remember, in D, you can define structs inside of functions 11295 // and those structs can access the function's local variables. 11296 // 11297 // Of course, you might also want to do this separately, and if you 11298 // do, make sure you keep a reference to the window as a struct data 11299 // member so you can refer to it in cases like this Exit function. 11300 struct Commands { 11301 // the & in the string indicates that the next letter is the hotkey 11302 // to access it from the keyboard (so here, alt+f will open the 11303 // file menu) 11304 @menu("&File") { 11305 @accelerator("Ctrl+N") 11306 @hotkey('n') 11307 @icon(GenericIcons.New) // add an icon to the action 11308 @toolbar("File") // adds it to a toolbar. 11309 // The toolbar name is never visible to the user, but is used to group icons. 11310 void New() { 11311 previousFileReferenced = null; 11312 textEdit.content = ""; 11313 } 11314 11315 @icon(GenericIcons.Open) 11316 @toolbar("File") 11317 @hotkey('s') 11318 @accelerator("Ctrl+O") 11319 void Open(FileName!() filename) { 11320 import std.file; 11321 textEdit.content = std.file.readText(filename); 11322 } 11323 11324 @icon(GenericIcons.Save) 11325 @toolbar("File") 11326 @accelerator("Ctrl+S") 11327 @hotkey('s') 11328 void Save() { 11329 // these are still functions, so of course you can 11330 // still call them yourself too 11331 Save_As(previousFileReferenced); 11332 } 11333 11334 // underscores translate to spaces in the visible name 11335 @hotkey('a') 11336 void Save_As(FileName!() filename) { 11337 import std.file; 11338 std.file.write(previousFileReferenced, textEdit.content); 11339 } 11340 11341 // you can put the annotations before or after the function name+args and it works the same way 11342 @separator 11343 void Exit() @accelerator("Alt+F4") @hotkey('x') { 11344 window.close(); 11345 } 11346 } 11347 11348 @menu("&Edit") { 11349 // not putting accelerators here because the text edit widget 11350 // does it locally, so no need to duplicate it globally. 11351 11352 @icon(GenericIcons.Undo) 11353 void Undo() @toolbar("Undo") { 11354 textEdit.undo(); 11355 } 11356 11357 @separator 11358 11359 @icon(GenericIcons.Cut) 11360 void Cut() @toolbar("Edit") { 11361 textEdit.cut(); 11362 } 11363 @icon(GenericIcons.Copy) 11364 void Copy() @toolbar("Edit") { 11365 textEdit.copy(); 11366 } 11367 @icon(GenericIcons.Paste) 11368 void Paste() @toolbar("Edit") { 11369 textEdit.paste(); 11370 } 11371 11372 @separator 11373 void Select_All() { 11374 textEdit.selectAll(); 11375 } 11376 } 11377 11378 @menu("Help") { 11379 void About() @accelerator("F1") { 11380 window.messageBox("A minigui sample program."); 11381 } 11382 11383 // @label changes the name in the menu from what is in the code 11384 @label("In Menu Name") 11385 void otherNameInCode() {} 11386 } 11387 } 11388 11389 // declare the object that holds the commands, and set 11390 // and members you want from it 11391 Commands commands; 11392 11393 // and now tell minigui to do its magic and create the ui for it! 11394 window.setMenuAndToolbarFromAnnotatedCode(commands); 11395 11396 // then, loop the window normally; 11397 window.loop(); 11398 11399 // important to note that the `commands` variable must live through the window's whole life cycle, 11400 // or you can have crashes. If you declare the variable and loop in different functions, make sure 11401 // you do `new Commands` so the garbage collector can take over management of it for you. 11402 } 11403 --- 11404 11405 Note that you can call this function multiple times and it will add the items in order to the given items. 11406 11407 +/ 11408 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 11409 setMenuAndToolbarFromAnnotatedCode_internal(t); 11410 } 11411 /// ditto 11412 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 11413 setMenuAndToolbarFromAnnotatedCode_internal(t); 11414 } 11415 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 11416 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 11417 Menu[string] mcs; 11418 11419 alias ToolbarSection = ToolBar.ToolbarSection; 11420 ToolbarSection[] toolbarSections; 11421 11422 foreach(menu; menuBar.subMenus) { 11423 mcs[menu.label] = menu; 11424 } 11425 11426 foreach(memberName; __traits(derivedMembers, T)) { 11427 static if(memberName != "this") 11428 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 11429 .menu menu; 11430 .toolbar toolbar; 11431 bool separator; 11432 .accelerator accelerator; 11433 .hotkey hotkey; 11434 .icon icon; 11435 string label; 11436 string tip; 11437 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 11438 static if(is(typeof(attr) == .menu)) 11439 menu = attr; 11440 else static if(is(typeof(attr) == .toolbar)) 11441 toolbar = attr; 11442 else static if(is(attr == .separator)) 11443 separator = true; 11444 else static if(is(typeof(attr) == .accelerator)) 11445 accelerator = attr; 11446 else static if(is(typeof(attr) == .hotkey)) 11447 hotkey = attr; 11448 else static if(is(typeof(attr) == .icon)) 11449 icon = attr; 11450 else static if(is(typeof(attr) == .label)) 11451 label = attr.label; 11452 else static if(is(typeof(attr) == .tip)) 11453 tip = attr.tip; 11454 } 11455 11456 if(menu !is .menu.init || toolbar !is .toolbar.init) { 11457 ushort correctIcon = icon.id; // FIXME 11458 if(label.length == 0) 11459 label = memberName.toMenuLabel; 11460 11461 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName)); 11462 11463 auto action = new Action(label, correctIcon, handler); 11464 11465 if(accelerator.keyString.length) { 11466 auto ke = KeyEvent.parse(accelerator.keyString); 11467 action.accelerator = ke; 11468 accelerators[ke.toStr] = handler; 11469 } 11470 11471 if(toolbar !is .toolbar.init) { 11472 bool found; 11473 foreach(ref section; toolbarSections) 11474 if(section.name == toolbar.groupName) { 11475 section.actions ~= action; 11476 found = true; 11477 break; 11478 } 11479 if(!found) { 11480 toolbarSections ~= ToolbarSection(toolbar.groupName, [action]); 11481 } 11482 } 11483 if(menu !is .menu.init) { 11484 Menu mc; 11485 if(menu.name in mcs) { 11486 mc = mcs[menu.name]; 11487 } else { 11488 mc = new Menu(menu.name, this); 11489 menuBar.addItem(mc); 11490 mcs[menu.name] = mc; 11491 } 11492 11493 if(separator) 11494 mc.addSeparator(); 11495 auto mi = mc.addItem(new MenuItem(action)); 11496 11497 if(hotkey !is .hotkey.init) 11498 mi.hotkey = hotkey.ch; 11499 } 11500 } 11501 } 11502 } 11503 11504 this.menuBar = menuBar; 11505 11506 if(toolbarSections.length) { 11507 auto tb = new ToolBar(toolbarSections, this); 11508 } 11509 } 11510 11511 void delegate()[string] accelerators; 11512 11513 override void defaultEventHandler_keydown(KeyDownEvent event) { 11514 auto str = event.originalKeyEvent.toStr; 11515 if(auto acl = str in accelerators) 11516 (*acl)(); 11517 11518 // Windows this this automatically so only on custom need we implement it 11519 version(custom_widgets) { 11520 if(event.altKey && this.menuBar) { 11521 foreach(item; this.menuBar.items) { 11522 if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) { 11523 // FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code 11524 item.dynamicState = DynamicState.hover | DynamicState.depressed; 11525 item.redraw(); 11526 auto e = new MouseDownEvent(item); 11527 e.dispatch(); 11528 break; 11529 } 11530 } 11531 } 11532 11533 if(event.key == Key.Menu) { 11534 showContextMenu(-1, -1); 11535 } 11536 } 11537 11538 super.defaultEventHandler_keydown(event); 11539 } 11540 11541 override void defaultEventHandler_mouseover(MouseOverEvent event) { 11542 super.defaultEventHandler_mouseover(event); 11543 if(this.statusBar !is null && event.target.statusTip.length) 11544 this.statusBar.parts[0].content = event.target.statusTip; 11545 else if(this.statusBar !is null && this.statusTip.length) 11546 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 11547 } 11548 11549 override void addChild(Widget c, int position = int.max) { 11550 if(auto tb = cast(ToolBar) c) 11551 version(win32_widgets) 11552 super.addChild(c, 0); 11553 else version(custom_widgets) 11554 super.addChild(c, menuBar ? 1 : 0); 11555 else static assert(0); 11556 else 11557 clientArea.addChild(c, position); 11558 } 11559 11560 ToolBar _toolBar; 11561 /// 11562 ToolBar toolBar() { return _toolBar; } 11563 /// 11564 ToolBar toolBar(ToolBar t) { 11565 _toolBar = t; 11566 foreach(child; this.children) 11567 if(child is t) 11568 return t; 11569 version(win32_widgets) 11570 super.addChild(t, 0); 11571 else version(custom_widgets) 11572 super.addChild(t, menuBar ? 1 : 0); 11573 else static assert(0); 11574 return t; 11575 } 11576 11577 MenuBar _menu; 11578 /// 11579 MenuBar menuBar() { return _menu; } 11580 /// 11581 MenuBar menuBar(MenuBar m) { 11582 if(m is _menu) { 11583 version(custom_widgets) 11584 queueRecomputeChildLayout(); 11585 return m; 11586 } 11587 11588 if(_menu !is null) { 11589 // make sure it is sanely removed 11590 // FIXME 11591 } 11592 11593 _menu = m; 11594 11595 version(win32_widgets) { 11596 SetMenu(parentWindow.win.impl.hwnd, m.handle); 11597 } else version(custom_widgets) { 11598 super.addChild(m, 0); 11599 11600 // clientArea.y = menu.height; 11601 // clientArea.height = this.height - menu.height; 11602 11603 queueRecomputeChildLayout(); 11604 } else static assert(false); 11605 11606 return _menu; 11607 } 11608 private Widget _clientArea; 11609 /// 11610 @property Widget clientArea() { return _clientArea; } 11611 protected @property void clientArea(Widget wid) { 11612 _clientArea = wid; 11613 } 11614 11615 private StatusBar _statusBar; 11616 /++ 11617 Returns the window's [StatusBar]. Be warned it may be `null`. 11618 +/ 11619 @property StatusBar statusBar() { return _statusBar; } 11620 /// ditto 11621 @property void statusBar(StatusBar bar) { 11622 if(_statusBar !is null) 11623 _statusBar.removeWidget(); 11624 _statusBar = bar; 11625 if(bar !is null) 11626 super.addChild(_statusBar); 11627 } 11628 } 11629 11630 /+ 11631 This is really an implementation detail of [MainWindow] 11632 +/ 11633 private class ClientAreaWidget : Widget { 11634 this() { 11635 this.tabStop = false; 11636 super(null); 11637 //sa = new ScrollableWidget(this); 11638 } 11639 /* 11640 ScrollableWidget sa; 11641 override void addChild(Widget w, int position) { 11642 if(sa is null) 11643 super.addChild(w, position); 11644 else { 11645 sa.addChild(w, position); 11646 sa.setContentSize(this.minWidth + 1, this.minHeight); 11647 writeln(sa.contentWidth, "x", sa.contentHeight); 11648 } 11649 } 11650 */ 11651 } 11652 11653 /** 11654 Toolbars are lists of buttons (typically icons) that appear under the menu. 11655 Each button ought to correspond to a menu item, represented by [Action] objects. 11656 */ 11657 class ToolBar : Widget { 11658 version(win32_widgets) { 11659 private int idealHeight; 11660 override int minHeight() { return idealHeight; } 11661 override int maxHeight() { return idealHeight; } 11662 } else version(custom_widgets) { 11663 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 11664 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 11665 } else static assert(false); 11666 override int heightStretchiness() { return 0; } 11667 11668 static struct ToolbarSection { 11669 string name; 11670 Action[] actions; 11671 } 11672 11673 version(win32_widgets) { 11674 HIMAGELIST imageListSmall; 11675 HIMAGELIST imageListLarge; 11676 } 11677 11678 this(Widget parent) { 11679 this(cast(ToolbarSection[]) null, parent); 11680 } 11681 11682 version(win32_widgets) 11683 void changeIconSize(bool useLarge) { 11684 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 11685 11686 /+ 11687 SIZE size; 11688 import core.sys.windows.commctrl; 11689 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 11690 idealHeight = size.cy + 4; // the plus 4 is a hack 11691 +/ 11692 11693 idealHeight = useLarge ? 34 : 26; 11694 11695 if(parent) { 11696 parent.queueRecomputeChildLayout(); 11697 parent.redraw(); 11698 } 11699 11700 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 11701 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 11702 } 11703 11704 /++ 11705 History: 11706 The `ToolbarSection` overload was added December 31, 2024 11707 +/ 11708 this(Action[] actions, Widget parent) { 11709 this([ToolbarSection(null, actions)], parent); 11710 } 11711 11712 /// ditto 11713 this(ToolbarSection[] sections, Widget parent) { 11714 super(parent); 11715 11716 tabStop = false; 11717 11718 version(win32_widgets) { 11719 // so i like how the flat thing looks on windows, but not on wine 11720 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 11721 // leave it commented 11722 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 11723 11724 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 11725 11726 imageListSmall = ImageList_Create( 11727 // width, height 11728 16, 16, 11729 ILC_COLOR16 | ILC_MASK, 11730 16 /*numberOfButtons*/, 0); 11731 11732 imageListLarge = ImageList_Create( 11733 // width, height 11734 24, 24, 11735 ILC_COLOR16 | ILC_MASK, 11736 16 /*numberOfButtons*/, 0); 11737 11738 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 11739 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 11740 11741 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 11742 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 11743 11744 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 11745 11746 TBBUTTON[] buttons; 11747 11748 // FIXME: I_IMAGENONE is if here is no icon 11749 foreach(sidx, section; sections) { 11750 if(sidx) 11751 buttons ~= TBBUTTON( 11752 scaleWithDpi(4), 11753 0, 11754 TBSTATE_ENABLED, // state 11755 TBSTYLE_SEP | BTNS_SEP, // style 11756 0, // reserved array, just zero it out 11757 0, // dwData 11758 -1 11759 ); 11760 11761 foreach(action; section.actions) 11762 buttons ~= TBBUTTON( 11763 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 11764 action.id, 11765 TBSTATE_ENABLED, // state 11766 0, // style 11767 0, // reserved array, just zero it out 11768 0, // dwData 11769 cast(size_t) toWstringzInternal(action.label) // INT_PTR 11770 ); 11771 } 11772 11773 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 11774 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 11775 11776 /* 11777 RECT rect; 11778 GetWindowRect(hwnd, &rect); 11779 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 11780 */ 11781 11782 dpiChanged(); // to load the things calling changeIconSize the first time 11783 11784 assert(idealHeight); 11785 } else version(custom_widgets) { 11786 foreach(sidx, section; sections) { 11787 if(sidx) 11788 new HorizontalSpacer(4, this); 11789 foreach(action; section.actions) 11790 new ToolButton(action, this); 11791 } 11792 } else static assert(false); 11793 } 11794 11795 override void recomputeChildLayout() { 11796 .recomputeChildLayout!"width"(this); 11797 } 11798 11799 11800 version(win32_widgets) 11801 override protected void dpiChanged() { 11802 auto sz = scaleWithDpi(16); 11803 if(sz >= 20) 11804 changeIconSize(true); 11805 else 11806 changeIconSize(false); 11807 } 11808 } 11809 11810 /// 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. 11811 class ToolButton : Button { 11812 /// 11813 this(Action action, Widget parent) { 11814 super(action.label, parent); 11815 tabStop = false; 11816 this.action = action; 11817 } 11818 11819 version(custom_widgets) 11820 override void defaultEventHandler_click(ClickEvent event) { 11821 foreach(handler; action.triggered) 11822 handler(); 11823 } 11824 11825 Action action; 11826 11827 override int maxWidth() { return toolbarIconSize; } 11828 override int minWidth() { return toolbarIconSize; } 11829 override int maxHeight() { return toolbarIconSize; } 11830 override int minHeight() { return toolbarIconSize; } 11831 11832 version(custom_widgets) 11833 override void paint(WidgetPainter painter) { 11834 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 11835 painter.outlineColor = Color.black; 11836 11837 immutable multiplier = toolbarIconSize / 4; 11838 immutable divisor = 16 / 4; 11839 11840 int ScaledNumber(int n) { 11841 // return n * multiplier / divisor; 11842 auto s = n * multiplier; 11843 auto it = s / divisor; 11844 auto rem = s % divisor; 11845 if(rem && n >= 8) // cuz the original used 0 .. 16 and we want to try to stay centered so things in the bottom half tend to be added a it 11846 it++; 11847 return it; 11848 } 11849 11850 arsd.color.Point Point(int x, int y) { 11851 return arsd.color.Point(ScaledNumber(x), ScaledNumber(y)); 11852 } 11853 11854 switch(action.iconId) { 11855 case GenericIcons.New: 11856 painter.fillColor = Color.white; 11857 painter.drawPolygon( 11858 Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6), 11859 Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2), 11860 Point(3, 2), Point(3, 13) 11861 ); 11862 break; 11863 case GenericIcons.Save: 11864 painter.fillColor = Color.white; 11865 painter.outlineColor = Color.black; 11866 painter.drawRectangle(Point(2, 2), Point(13, 13)); 11867 11868 // the label 11869 painter.drawRectangle(Point(4, 8), Point(11, 13)); 11870 11871 // the slider 11872 painter.fillColor = Color.black; 11873 painter.outlineColor = Color.black; 11874 painter.drawRectangle(Point(4, 3), Point(10, 6)); 11875 11876 painter.fillColor = Color.white; 11877 painter.outlineColor = Color.white; 11878 // the disc window 11879 painter.drawRectangle(Point(5, 3), Point(6, 5)); 11880 break; 11881 case GenericIcons.Open: 11882 painter.fillColor = Color.white; 11883 painter.drawPolygon( 11884 Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3), 11885 Point(9, 3), Point(9, 4), Point(4, 4)); 11886 painter.drawPolygon( 11887 Point(2, 6), Point(11, 6), 11888 Point(12, 12), Point(4, 12), 11889 Point(2, 6)); 11890 //painter.drawLine(Point(9, 6), Point(13, 7)); 11891 break; 11892 case GenericIcons.Copy: 11893 painter.fillColor = Color.white; 11894 painter.drawRectangle(Point(3, 2), Point(9, 10)); 11895 painter.drawRectangle(Point(6, 5), Point(12, 13)); 11896 break; 11897 case GenericIcons.Cut: 11898 painter.fillColor = Color.transparent; 11899 painter.outlineColor = getComputedStyle.foregroundColor(); 11900 painter.drawLine(Point(3, 2), Point(10, 9)); 11901 painter.drawLine(Point(4, 9), Point(11, 2)); 11902 painter.drawRectangle(Point(3, 9), Point(5, 13)); 11903 painter.drawRectangle(Point(9, 9), Point(11, 12)); 11904 break; 11905 case GenericIcons.Paste: 11906 painter.fillColor = Color.white; 11907 painter.drawRectangle(Point(2, 3), Point(11, 11)); 11908 painter.drawRectangle(Point(6, 8), Point(13, 13)); 11909 painter.drawLine(Point(6, 2), Point(4, 5)); 11910 painter.drawLine(Point(6, 2), Point(9, 5)); 11911 painter.fillColor = Color.black; 11912 painter.drawRectangle(Point(4, 5), Point(9, 6)); 11913 break; 11914 case GenericIcons.Help: 11915 painter.outlineColor = getComputedStyle.foregroundColor(); 11916 painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11917 break; 11918 case GenericIcons.Undo: 11919 painter.fillColor = Color.transparent; 11920 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 11921 painter.outlineColor = Color.black; 11922 painter.fillColor = Color.black; 11923 painter.drawPolygon( 11924 Point(4, 4), 11925 Point(8, 2), 11926 Point(8, 6), 11927 Point(4, 4), 11928 ); 11929 break; 11930 case GenericIcons.Redo: 11931 painter.fillColor = Color.transparent; 11932 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 11933 painter.outlineColor = Color.black; 11934 painter.fillColor = Color.black; 11935 painter.drawPolygon( 11936 Point(10, 4), 11937 Point(6, 2), 11938 Point(6, 6), 11939 Point(10, 4), 11940 ); 11941 break; 11942 default: 11943 painter.outlineColor = getComputedStyle.foregroundColor; 11944 painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11945 } 11946 return bounds; 11947 }); 11948 } 11949 11950 } 11951 11952 11953 /++ 11954 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 11955 +/ 11956 class MenuBar : Widget { 11957 MenuItem[] items; 11958 Menu[] subMenus; 11959 11960 version(win32_widgets) { 11961 HMENU handle; 11962 /// 11963 this(Widget parent = null) { 11964 super(parent); 11965 11966 handle = CreateMenu(); 11967 tabStop = false; 11968 } 11969 } else version(custom_widgets) { 11970 /// 11971 this(Widget parent = null) { 11972 tabStop = false; // these are selected some other way 11973 super(parent); 11974 } 11975 11976 mixin Padding!q{2}; 11977 } else static assert(false); 11978 11979 version(custom_widgets) 11980 override void paint(WidgetPainter painter) { 11981 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 11982 } 11983 11984 /// 11985 MenuItem addItem(MenuItem item) { 11986 this.addChild(item); 11987 items ~= item; 11988 version(win32_widgets) { 11989 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11990 } 11991 return item; 11992 } 11993 11994 11995 /// 11996 Menu addItem(Menu item) { 11997 11998 subMenus ~= item; 11999 12000 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 12001 12002 addChild(mbItem); 12003 items ~= mbItem; 12004 12005 version(win32_widgets) { 12006 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 12007 } else version(custom_widgets) { 12008 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 12009 item.popup(mbItem); 12010 }; 12011 } else static assert(false); 12012 12013 return item; 12014 } 12015 12016 override void recomputeChildLayout() { 12017 .recomputeChildLayout!"width"(this); 12018 } 12019 12020 override int maxHeight() { return defaultLineHeight + 4; } 12021 override int minHeight() { return defaultLineHeight + 4; } 12022 } 12023 12024 12025 /** 12026 Status bars appear at the bottom of a MainWindow. 12027 They are made out of Parts, with a width and content. 12028 12029 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 12030 12031 12032 sb.parts[0].content = "Status bar text!"; 12033 */ 12034 // https://learn.microsoft.com/en-us/windows/win32/controls/status-bars#owner-drawn-status-bars 12035 class StatusBar : Widget { 12036 private Part[] partsArray; 12037 /// 12038 struct Parts { 12039 @disable this(); 12040 this(StatusBar owner) { this.owner = owner; } 12041 //@disable this(this); 12042 /// 12043 @property int length() { return cast(int) owner.partsArray.length; } 12044 private StatusBar owner; 12045 private this(StatusBar owner, Part[] parts) { 12046 this.owner.partsArray = parts; 12047 this.owner = owner; 12048 } 12049 /// 12050 Part opIndex(int p) { 12051 if(owner.partsArray.length == 0) 12052 this ~= new StatusBar.Part(0); 12053 return owner.partsArray[p]; 12054 } 12055 12056 /// 12057 Part opOpAssign(string op : "~" )(Part p) { 12058 assert(owner.partsArray.length < 255); 12059 p.owner = this.owner; 12060 p.idx = cast(int) owner.partsArray.length; 12061 owner.partsArray ~= p; 12062 12063 owner.queueRecomputeChildLayout(); 12064 12065 version(win32_widgets) { 12066 int[256] pos; 12067 int cpos; 12068 foreach(idx, part; owner.partsArray) { 12069 if(idx + 1 == owner.partsArray.length) 12070 pos[idx] = -1; 12071 else { 12072 cpos += part.currentlyAssignedWidth; 12073 pos[idx] = cpos; 12074 } 12075 } 12076 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 12077 } else version(custom_widgets) { 12078 owner.redraw(); 12079 } else static assert(false); 12080 12081 return p; 12082 } 12083 12084 /++ 12085 Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions. 12086 12087 No given item should be 0. 12088 12089 History: 12090 Added December 31, 2024 12091 +/ 12092 void setSizes(int[] proportions...) { 12093 assert(this.owner); 12094 this.owner.partsArray = null; 12095 12096 foreach(n; proportions) { 12097 assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead."); 12098 12099 this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels)); 12100 } 12101 12102 } 12103 } 12104 12105 private Parts _parts; 12106 /// 12107 final @property Parts parts() { 12108 return _parts; 12109 } 12110 12111 /++ 12112 12113 +/ 12114 static class Part { 12115 /++ 12116 History: 12117 Added September 1, 2023 (dub v11.1) 12118 +/ 12119 enum WidthUnits { 12120 /++ 12121 Unscaled pixels as they appear on screen. 12122 12123 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 12124 +/ 12125 DeviceDependentPixels, 12126 /++ 12127 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 12128 +/ 12129 DeviceIndependentPixels, 12130 /++ 12131 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`). 12132 +/ 12133 ApproximateCharacters, 12134 /++ 12135 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. 12136 12137 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. 12138 +/ 12139 Proportional 12140 } 12141 private WidthUnits units; 12142 private int width; 12143 private StatusBar owner; 12144 12145 private int currentlyAssignedWidth; 12146 12147 /++ 12148 History: 12149 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. 12150 12151 It now allows you to provide your own value for [WidthUnits]. 12152 12153 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`. 12154 +/ 12155 this(int w, WidthUnits units = WidthUnits.Proportional) { 12156 this.units = units; 12157 this.width = w; 12158 } 12159 12160 /// ditto 12161 this(int w = 0) { 12162 if(w == 0) 12163 this(w, WidthUnits.Proportional); 12164 else 12165 this(w, WidthUnits.DeviceDependentPixels); 12166 } 12167 12168 private int idx; 12169 private string _content; 12170 /// 12171 @property string content() { return _content; } 12172 /// 12173 @property void content(string s) { 12174 version(win32_widgets) { 12175 _content = s; 12176 WCharzBuffer bfr = WCharzBuffer(s); 12177 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 12178 } else version(custom_widgets) { 12179 if(_content != s) { 12180 _content = s; 12181 owner.redraw(); 12182 } 12183 } else static assert(false); 12184 } 12185 } 12186 string simpleModeContent; 12187 bool inSimpleMode; 12188 12189 12190 /// 12191 this(Widget parent) { 12192 super(null); // FIXME 12193 _parts = Parts(this); 12194 tabStop = false; 12195 version(win32_widgets) { 12196 parentWindow = parent.parentWindow; 12197 createWin32Window(this, "msctls_statusbar32"w, "", 0); 12198 12199 RECT rect; 12200 GetWindowRect(hwnd, &rect); 12201 idealHeight = rect.bottom - rect.top; 12202 assert(idealHeight); 12203 } else version(custom_widgets) { 12204 } else static assert(false); 12205 } 12206 12207 override void recomputeChildLayout() { 12208 int remainingLength = this.width; 12209 12210 int proportionalSum; 12211 int proportionalCount; 12212 foreach(idx, part; this.partsArray) { 12213 with(Part.WidthUnits) 12214 final switch(part.units) { 12215 case DeviceDependentPixels: 12216 part.currentlyAssignedWidth = part.width; 12217 remainingLength -= part.currentlyAssignedWidth; 12218 break; 12219 case DeviceIndependentPixels: 12220 part.currentlyAssignedWidth = scaleWithDpi(part.width); 12221 remainingLength -= part.currentlyAssignedWidth; 12222 break; 12223 case ApproximateCharacters: 12224 auto cs = getComputedStyle(); 12225 auto font = cs.font; 12226 12227 part.currentlyAssignedWidth = castFnumToCnum(font.averageWidth * this.width); 12228 remainingLength -= part.currentlyAssignedWidth; 12229 break; 12230 case Proportional: 12231 proportionalSum += part.width; 12232 proportionalCount ++; 12233 break; 12234 } 12235 } 12236 12237 foreach(part; this.partsArray) { 12238 if(part.units == Part.WidthUnits.Proportional) { 12239 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 12240 if(proportion == 0) 12241 proportion = 1; 12242 12243 if(proportionalSum == 0) 12244 proportionalSum = proportionalCount; 12245 12246 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 12247 } 12248 } 12249 12250 super.recomputeChildLayout(); 12251 } 12252 12253 version(win32_widgets) 12254 override protected void dpiChanged() { 12255 RECT rect; 12256 GetWindowRect(hwnd, &rect); 12257 idealHeight = rect.bottom - rect.top; 12258 assert(idealHeight); 12259 } 12260 12261 version(custom_widgets) 12262 override void paint(WidgetPainter painter) { 12263 auto cs = getComputedStyle(); 12264 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12265 int cpos = 0; 12266 foreach(idx, part; this.partsArray) { 12267 auto partWidth = part.currentlyAssignedWidth; 12268 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 12269 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 12270 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 12271 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 12272 12273 painter.outlineColor = cs.foregroundColor(); 12274 painter.fillColor = cs.foregroundColor(); 12275 12276 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 12277 cpos += partWidth; 12278 } 12279 } 12280 12281 12282 version(win32_widgets) { 12283 private int idealHeight; 12284 override int maxHeight() { return idealHeight; } 12285 override int minHeight() { return idealHeight; } 12286 } else version(custom_widgets) { 12287 override int maxHeight() { return defaultLineHeight + 4; } 12288 override int minHeight() { return defaultLineHeight + 4; } 12289 } else static assert(false); 12290 } 12291 12292 /// Displays an in-progress indicator without known values 12293 version(none) 12294 class IndefiniteProgressBar : Widget { 12295 version(win32_widgets) 12296 this(Widget parent) { 12297 super(parent); 12298 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 12299 tabStop = false; 12300 } 12301 override int minHeight() { return 10; } 12302 } 12303 12304 /// A progress bar with a known endpoint and completion amount 12305 class ProgressBar : Widget { 12306 /++ 12307 History: 12308 Added March 16, 2022 (dub v10.7) 12309 +/ 12310 this(int min, int max, Widget parent) { 12311 this(parent); 12312 setRange(cast(ushort) min, cast(ushort) max); // FIXME 12313 } 12314 this(Widget parent) { 12315 version(win32_widgets) { 12316 super(parent); 12317 createWin32Window(this, "msctls_progress32"w, "", 0); 12318 tabStop = false; 12319 } else version(custom_widgets) { 12320 super(parent); 12321 max = 100; 12322 step = 10; 12323 tabStop = false; 12324 } else static assert(0); 12325 } 12326 12327 version(custom_widgets) 12328 override void paint(WidgetPainter painter) { 12329 auto cs = getComputedStyle(); 12330 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12331 painter.fillColor = cs.progressBarColor; 12332 painter.drawRectangle(Point(0, 0), width * current / max, height); 12333 } 12334 12335 12336 version(custom_widgets) { 12337 int current; 12338 int max; 12339 int step; 12340 } 12341 12342 /// 12343 void advanceOneStep() { 12344 version(win32_widgets) 12345 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 12346 else version(custom_widgets) 12347 addToPosition(step); 12348 else static assert(false); 12349 } 12350 12351 /// 12352 void setStepIncrement(int increment) { 12353 version(win32_widgets) 12354 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 12355 else version(custom_widgets) 12356 step = increment; 12357 else static assert(false); 12358 } 12359 12360 /// 12361 void addToPosition(int amount) { 12362 version(win32_widgets) 12363 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 12364 else version(custom_widgets) 12365 setPosition(current + amount); 12366 else static assert(false); 12367 } 12368 12369 /// 12370 void setPosition(int pos) { 12371 version(win32_widgets) 12372 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 12373 else version(custom_widgets) { 12374 current = pos; 12375 if(current > max) 12376 current = max; 12377 redraw(); 12378 } 12379 else static assert(false); 12380 } 12381 12382 /// 12383 void setRange(ushort min, ushort max) { 12384 version(win32_widgets) 12385 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 12386 else version(custom_widgets) { 12387 this.max = max; 12388 } 12389 else static assert(false); 12390 } 12391 12392 override int minHeight() { return 10; } 12393 } 12394 12395 version(custom_widgets) 12396 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 12397 thisLabel.reserve(label.length); 12398 bool justSawAmpersand; 12399 foreach(ch; label) { 12400 if(justSawAmpersand) { 12401 justSawAmpersand = false; 12402 if(ch == '&') { 12403 goto plain; 12404 } 12405 thisAccelerator = ch; 12406 } else { 12407 if(ch == '&') { 12408 justSawAmpersand = true; 12409 continue; 12410 } 12411 plain: 12412 thisLabel ~= ch; 12413 } 12414 } 12415 } 12416 12417 /++ 12418 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. 12419 12420 12421 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 12422 12423 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12424 12425 History: 12426 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. 12427 +/ 12428 class Fieldset : Widget { 12429 // FIXME: on Windows,it doesn't draw the background on the label 12430 // on X, it doesn't fix the clipping rectangle for it 12431 version(win32_widgets) 12432 override int paddingTop() { return defaultLineHeight; } 12433 else version(custom_widgets) 12434 override int paddingTop() { return defaultLineHeight + 2; } 12435 else static assert(false); 12436 override int paddingBottom() { return 6; } 12437 override int paddingLeft() { return 6; } 12438 override int paddingRight() { return 6; } 12439 12440 override int marginLeft() { return 6; } 12441 override int marginRight() { return 6; } 12442 override int marginTop() { return 2; } 12443 override int marginBottom() { return 2; } 12444 12445 string legend; 12446 12447 version(custom_widgets) private dchar accelerator; 12448 12449 this(string legend, Widget parent) { 12450 version(win32_widgets) { 12451 super(parent); 12452 this.legend = legend; 12453 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 12454 tabStop = false; 12455 } else version(custom_widgets) { 12456 super(parent); 12457 tabStop = false; 12458 12459 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 12460 } else static assert(0); 12461 } 12462 12463 version(custom_widgets) 12464 override void paint(WidgetPainter painter) { 12465 auto dlh = defaultLineHeight; 12466 12467 painter.fillColor = Color.transparent; 12468 auto cs = getComputedStyle(); 12469 painter.pen = Pen(cs.foregroundColor, 1); 12470 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 12471 12472 auto tx = painter.textSize(legend); 12473 painter.outlineColor = Color.transparent; 12474 12475 version(Windows) { 12476 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 12477 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 12478 SelectObject(painter.impl.hdc, b); 12479 } else static if(UsingSimpledisplayX11) { 12480 painter.fillColor = getComputedStyle().windowBackgroundColor; 12481 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 12482 } 12483 painter.outlineColor = cs.foregroundColor; 12484 painter.drawText(Point(8, 0), legend); 12485 } 12486 12487 override int maxHeight() { 12488 auto m = paddingTop() + paddingBottom(); 12489 foreach(child; children) { 12490 auto mh = child.maxHeight(); 12491 if(mh == int.max) 12492 return int.max; 12493 m += mh; 12494 m += child.marginBottom(); 12495 m += child.marginTop(); 12496 } 12497 m += 6; 12498 if(m < minHeight) 12499 return minHeight; 12500 return m; 12501 } 12502 12503 override int minHeight() { 12504 auto m = paddingTop() + paddingBottom(); 12505 foreach(child; children) { 12506 m += child.minHeight(); 12507 m += child.marginBottom(); 12508 m += child.marginTop(); 12509 } 12510 return m + 6; 12511 } 12512 12513 override int minWidth() { 12514 return 6 + cast(int) this.legend.length * 7; 12515 } 12516 } 12517 12518 /++ 12519 $(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") 12520 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 12521 +/ 12522 version(minigui_screenshots) 12523 @Screenshot("Fieldset") 12524 unittest { 12525 auto window = new Window(200, 100); 12526 auto set = new Fieldset("Baby will", window); 12527 auto option1 = new Radiobox("Eat", set); 12528 auto option2 = new Radiobox("Cry", set); 12529 auto option3 = new Radiobox("Sleep", set); 12530 window.loop(); 12531 } 12532 12533 /// Draws a line 12534 class HorizontalRule : Widget { 12535 mixin Margin!q{ 2 }; 12536 override int minHeight() { return 2; } 12537 override int maxHeight() { return 2; } 12538 12539 /// 12540 this(Widget parent) { 12541 super(parent); 12542 } 12543 12544 override void paint(WidgetPainter painter) { 12545 auto cs = getComputedStyle(); 12546 painter.outlineColor = cs.darkAccentColor; 12547 painter.drawLine(Point(0, 0), Point(width, 0)); 12548 painter.outlineColor = cs.lightAccentColor; 12549 painter.drawLine(Point(0, 1), Point(width, 1)); 12550 } 12551 } 12552 12553 version(minigui_screenshots) 12554 @Screenshot("HorizontalRule") 12555 /++ 12556 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 12557 12558 +/ 12559 unittest { 12560 auto window = new Window(200, 100); 12561 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 12562 new HorizontalRule(window); 12563 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 12564 window.loop(); 12565 } 12566 12567 /// ditto 12568 class VerticalRule : Widget { 12569 mixin Margin!q{ 2 }; 12570 override int minWidth() { return 2; } 12571 override int maxWidth() { return 2; } 12572 12573 /// 12574 this(Widget parent) { 12575 super(parent); 12576 } 12577 12578 override void paint(WidgetPainter painter) { 12579 auto cs = getComputedStyle(); 12580 painter.outlineColor = cs.darkAccentColor; 12581 painter.drawLine(Point(0, 0), Point(0, height)); 12582 painter.outlineColor = cs.lightAccentColor; 12583 painter.drawLine(Point(1, 0), Point(1, height)); 12584 } 12585 } 12586 12587 12588 /// 12589 class Menu : Window { 12590 void remove() { 12591 foreach(i, child; parentWindow.children) 12592 if(child is this) { 12593 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 12594 break; 12595 } 12596 parentWindow.redraw(); 12597 12598 parentWindow.releaseMouseCapture(); 12599 } 12600 12601 /// 12602 void addSeparator() { 12603 version(win32_widgets) 12604 AppendMenu(handle, MF_SEPARATOR, 0, null); 12605 else version(custom_widgets) 12606 auto hr = new HorizontalRule(this); 12607 else static assert(0); 12608 } 12609 12610 override int paddingTop() { return 4; } 12611 override int paddingBottom() { return 4; } 12612 override int paddingLeft() { return 2; } 12613 override int paddingRight() { return 2; } 12614 12615 version(win32_widgets) {} 12616 else version(custom_widgets) { 12617 12618 Widget previouslyFocusedWidget; 12619 Widget* previouslyFocusedWidgetBelongsIn; 12620 12621 SimpleWindow dropDown; 12622 Widget menuParent; 12623 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 12624 this.menuParent = parent; 12625 12626 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 12627 previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget; 12628 parent.parentWindow.focusedWidget = this; 12629 12630 int w = 150; 12631 int h = paddingTop + paddingBottom; 12632 if(this.children.length) { 12633 // hacking it to get the ideal height out of recomputeChildLayout 12634 this.width = w; 12635 this.height = h; 12636 this.recomputeChildLayoutEntry(); 12637 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 12638 h += paddingBottom; 12639 12640 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 12641 } 12642 12643 if(offsetY == int.min) 12644 offsetY = parent.defaultLineHeight; 12645 12646 auto coord = parent.globalCoordinates(); 12647 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 12648 this.x = 0; 12649 this.y = 0; 12650 this.width = dropDown.width; 12651 this.height = dropDown.height; 12652 this.drawableWindow = dropDown; 12653 this.recomputeChildLayoutEntry(); 12654 12655 static if(UsingSimpledisplayX11) 12656 XSync(XDisplayConnection.get, 0); 12657 12658 dropDown.visibilityChanged = (bool visible) { 12659 if(visible) { 12660 this.redraw(); 12661 dropDown.grabInput(); 12662 } else { 12663 dropDown.releaseInputGrab(); 12664 } 12665 }; 12666 12667 dropDown.show(); 12668 12669 clickListener = this.addEventListener((scope ClickEvent ev) { 12670 unpopup(); 12671 // need to unlock asap just in case other user handlers block... 12672 static if(UsingSimpledisplayX11) 12673 flushGui(); 12674 }, true /* again for asap action */); 12675 } 12676 12677 EventListener clickListener; 12678 } 12679 else static assert(false); 12680 12681 version(custom_widgets) 12682 void unpopup() { 12683 mouseLastOver = mouseLastDownOn = null; 12684 dropDown.hide(); 12685 if(!menuParent.parentWindow.win.closed) { 12686 if(auto maw = cast(MouseActivatedWidget) menuParent) { 12687 maw.setDynamicState(DynamicState.depressed, false); 12688 maw.setDynamicState(DynamicState.hover, false); 12689 maw.redraw(); 12690 } 12691 // menuParent.parentWindow.win.focus(); 12692 } 12693 clickListener.disconnect(); 12694 12695 if(previouslyFocusedWidgetBelongsIn) 12696 *previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget; 12697 } 12698 12699 MenuItem[] items; 12700 12701 /// 12702 MenuItem addItem(MenuItem item) { 12703 addChild(item); 12704 items ~= item; 12705 version(win32_widgets) { 12706 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 12707 } 12708 return item; 12709 } 12710 12711 string label; 12712 12713 version(win32_widgets) { 12714 HMENU handle; 12715 /// 12716 this(string label, Widget parent) { 12717 // not actually passing the parent since it effs up the drawing 12718 super(cast(Widget) null);// parent); 12719 this.label = label; 12720 handle = CreatePopupMenu(); 12721 } 12722 } else version(custom_widgets) { 12723 /// 12724 this(string label, Widget parent) { 12725 12726 if(dropDown) { 12727 dropDown.close(); 12728 } 12729 dropDown = new SimpleWindow( 12730 150, 4, 12731 // FIXME: what if it is a popupMenu ? 12732 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 12733 12734 this.label = label; 12735 12736 super(dropDown); 12737 } 12738 } else static assert(false); 12739 12740 override int maxHeight() { return defaultLineHeight; } 12741 override int minHeight() { return defaultLineHeight; } 12742 12743 version(custom_widgets) { 12744 Widget currentPlace; 12745 12746 void changeCurrentPlace(Widget n) { 12747 if(currentPlace) { 12748 currentPlace.dynamicState = 0; 12749 } 12750 12751 if(n) { 12752 n.dynamicState = DynamicState.hover; 12753 } 12754 12755 currentPlace = n; 12756 } 12757 12758 override void paint(WidgetPainter painter) { 12759 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 12760 } 12761 12762 override void defaultEventHandler_keydown(KeyDownEvent ke) { 12763 switch(ke.key) { 12764 case Key.Down: 12765 Widget next; 12766 Widget first; 12767 foreach(w; this.children) { 12768 if((cast(MenuItem) w) is null) 12769 continue; 12770 12771 if(first is null) 12772 first = w; 12773 12774 if(next !is null) { 12775 next = w; 12776 break; 12777 } 12778 12779 if(currentPlace is null) { 12780 next = w; 12781 break; 12782 } 12783 12784 if(w is currentPlace) { 12785 next = w; 12786 } 12787 } 12788 12789 if(next is currentPlace) 12790 next = first; 12791 12792 changeCurrentPlace(next); 12793 break; 12794 case Key.Up: 12795 Widget prev; 12796 foreach(w; this.children) { 12797 if((cast(MenuItem) w) is null) 12798 continue; 12799 if(w is currentPlace) { 12800 if(prev is null) { 12801 foreach_reverse(c; this.children) { 12802 if((cast(MenuItem) c) !is null) { 12803 prev = c; 12804 break; 12805 } 12806 } 12807 } 12808 break; 12809 } 12810 prev = w; 12811 } 12812 changeCurrentPlace(prev); 12813 break; 12814 case Key.Left: 12815 case Key.Right: 12816 if(menuParent) { 12817 Menu first; 12818 Menu last; 12819 Menu prev; 12820 Menu next; 12821 bool found; 12822 12823 size_t prev_idx; 12824 size_t next_idx; 12825 12826 MenuBar mb = cast(MenuBar) menuParent.parent; 12827 12828 if(mb) { 12829 foreach(idx, menu; mb.subMenus) { 12830 if(first is null) 12831 first = menu; 12832 last = menu; 12833 if(found && next is null) { 12834 next = menu; 12835 next_idx = idx; 12836 } 12837 if(menu is this) 12838 found = true; 12839 if(!found) { 12840 prev = menu; 12841 prev_idx = idx; 12842 } 12843 } 12844 12845 Menu nextMenu; 12846 size_t nextMenuIdx; 12847 if(ke.key == Key.Left) { 12848 nextMenu = prev ? prev : last; 12849 nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1; 12850 } else { 12851 nextMenu = next ? next : first; 12852 nextMenuIdx = next ? next_idx : 0; 12853 } 12854 12855 unpopup(); 12856 12857 auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right 12858 rent.dynamicState = DynamicState.depressed | DynamicState.hover; 12859 nextMenu.popup(rent); 12860 } 12861 } 12862 break; 12863 case Key.Enter: 12864 case Key.PadEnter: 12865 // because the key up and char events will go back to the other window after we unpopup! 12866 // we will wait for the char event to come (in the following method) 12867 break; 12868 case Key.Escape: 12869 unpopup(); 12870 break; 12871 default: 12872 } 12873 } 12874 override void defaultEventHandler_char(CharEvent ke) { 12875 // if one is selected, enter activates it 12876 if(currentPlace) { 12877 if(ke.character == '\n') { 12878 // enter selects 12879 auto event = new Event(EventType.triggered, currentPlace); 12880 event.dispatch(); 12881 unpopup(); 12882 return; 12883 } 12884 } 12885 12886 // otherwise search for a hotkey 12887 foreach(item; items) { 12888 if(item.hotkey == ke.character) { 12889 auto event = new Event(EventType.triggered, item); 12890 event.dispatch(); 12891 unpopup(); 12892 return; 12893 } 12894 } 12895 } 12896 override void defaultEventHandler_mouseover(MouseOverEvent moe) { 12897 if(moe.target && moe.target.parent is this) 12898 changeCurrentPlace(moe.target); 12899 } 12900 } 12901 } 12902 12903 /++ 12904 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 12905 +/ 12906 class MenuItem : MouseActivatedWidget { 12907 Menu submenu; 12908 12909 Action action; 12910 string label; 12911 dchar hotkey; 12912 12913 override int paddingLeft() { return 4; } 12914 12915 override int maxHeight() { return defaultLineHeight + 4; } 12916 override int minHeight() { return defaultLineHeight + 4; } 12917 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 12918 override int maxWidth() { 12919 if(cast(MenuBar) parent) { 12920 return minWidth(); 12921 } 12922 return int.max; 12923 } 12924 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 12925 this(string lbl, Widget parent = null) { 12926 super(parent); 12927 //label = lbl; // FIXME 12928 foreach(idx, char ch; lbl) // FIXME 12929 if(ch != '&') { // FIXME 12930 label ~= ch; // FIXME 12931 } else { 12932 if(idx + 1 < lbl.length) { 12933 hotkey = lbl[idx + 1]; 12934 if(hotkey >= 'A' && hotkey <= 'Z') 12935 hotkey += 32; 12936 } 12937 } 12938 tabStop = false; // these are selected some other way 12939 } 12940 12941 /// 12942 this(Action action, Widget parent = null) { 12943 assert(action !is null); 12944 this(action.label, parent); 12945 this.action = action; 12946 tabStop = false; // these are selected some other way 12947 } 12948 12949 version(custom_widgets) 12950 override void paint(WidgetPainter painter) { 12951 auto cs = getComputedStyle(); 12952 if(dynamicState & DynamicState.depressed) 12953 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12954 else { 12955 if(dynamicState & DynamicState.hover) { 12956 painter.fillColor = cs.hoveringColor; 12957 painter.outlineColor = Color.transparent; 12958 } else { 12959 painter.fillColor = cs.background.color; 12960 painter.outlineColor = Color.transparent; 12961 } 12962 12963 painter.drawRectangle(Point(0, 0), Size(this.width, this.height)); 12964 } 12965 12966 if(dynamicState & DynamicState.hover) 12967 painter.outlineColor = cs.activeMenuItemColor; 12968 else 12969 painter.outlineColor = cs.foregroundColor; 12970 painter.fillColor = Color.transparent; 12971 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 12972 if(action && action.accelerator !is KeyEvent.init) { 12973 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 12974 12975 } 12976 } 12977 12978 static class Style : Widget.Style { 12979 override bool variesWithState(ulong dynamicStateFlags) { 12980 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 12981 } 12982 } 12983 mixin OverrideStyle!Style; 12984 12985 override void defaultEventHandler_triggered(Event event) { 12986 if(action) 12987 foreach(handler; action.triggered) 12988 handler(); 12989 12990 if(auto pmenu = cast(Menu) this.parent) 12991 pmenu.remove(); 12992 12993 super.defaultEventHandler_triggered(event); 12994 } 12995 } 12996 12997 version(win32_widgets) 12998 /// A "mouse activiated widget" is really just an abstract variant of button. 12999 class MouseActivatedWidget : Widget { 13000 @property bool isChecked() { 13001 assert(hwnd); 13002 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 13003 13004 } 13005 @property void isChecked(bool state) { 13006 assert(hwnd); 13007 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 13008 13009 } 13010 13011 override void handleWmCommand(ushort cmd, ushort id) { 13012 if(cmd == 0) { 13013 auto event = new Event(EventType.triggered, this); 13014 event.dispatch(); 13015 } 13016 } 13017 13018 this(Widget parent) { 13019 super(parent); 13020 } 13021 } 13022 else version(custom_widgets) 13023 /// ditto 13024 class MouseActivatedWidget : Widget { 13025 @property bool isChecked() { return isChecked_; } 13026 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 13027 13028 private bool isChecked_; 13029 13030 this(Widget parent) { 13031 super(parent); 13032 13033 addEventListener((MouseDownEvent ev) { 13034 if(ev.button == MouseButton.left) { 13035 setDynamicState(DynamicState.depressed, true); 13036 setDynamicState(DynamicState.hover, true); 13037 redraw(); 13038 } 13039 }); 13040 13041 addEventListener((MouseUpEvent ev) { 13042 if(ev.button == MouseButton.left) { 13043 setDynamicState(DynamicState.depressed, false); 13044 setDynamicState(DynamicState.hover, false); 13045 redraw(); 13046 } 13047 }); 13048 13049 addEventListener((MouseMoveEvent mme) { 13050 if(!(mme.state & ModifierState.leftButtonDown)) { 13051 if(dynamicState_ & DynamicState.depressed) { 13052 setDynamicState(DynamicState.depressed, false); 13053 redraw(); 13054 } 13055 } 13056 }); 13057 } 13058 13059 override void defaultEventHandler_focus(FocusEvent ev) { 13060 super.defaultEventHandler_focus(ev); 13061 this.redraw(); 13062 } 13063 override void defaultEventHandler_blur(BlurEvent ev) { 13064 super.defaultEventHandler_blur(ev); 13065 setDynamicState(DynamicState.depressed, false); 13066 this.redraw(); 13067 } 13068 override void defaultEventHandler_keydown(KeyDownEvent ev) { 13069 super.defaultEventHandler_keydown(ev); 13070 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 13071 setDynamicState(DynamicState.depressed, true); 13072 setDynamicState(DynamicState.hover, true); 13073 this.redraw(); 13074 } 13075 } 13076 override void defaultEventHandler_keyup(KeyUpEvent ev) { 13077 super.defaultEventHandler_keyup(ev); 13078 if(!(dynamicState & DynamicState.depressed)) 13079 return; 13080 if(!enabled) 13081 return; 13082 setDynamicState(DynamicState.depressed, false); 13083 setDynamicState(DynamicState.hover, false); 13084 this.redraw(); 13085 13086 auto event = new Event(EventType.triggered, this); 13087 event.sendDirectly(); 13088 } 13089 override void defaultEventHandler_click(ClickEvent ev) { 13090 super.defaultEventHandler_click(ev); 13091 if(ev.button == MouseButton.left && enabled) { 13092 auto event = new Event(EventType.triggered, this); 13093 event.sendDirectly(); 13094 } 13095 } 13096 13097 } 13098 else static assert(false); 13099 13100 /* 13101 /++ 13102 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 13103 13104 Basically the same as a checkbox. 13105 +/ 13106 class OnOffSwitch : MouseActivatedWidget { 13107 13108 } 13109 */ 13110 13111 /++ 13112 History: 13113 Added June 15, 2021 (dub v10.1) 13114 +/ 13115 struct ImageLabel { 13116 /++ 13117 Defines a label+image combo used by some widgets. 13118 13119 If you provide just a text label, that is all the widget will try to 13120 display. Or just an image will display just that. If you provide both, 13121 it may display both text and image side by side or display the image 13122 and offer text on an input event depending on the widget. 13123 13124 History: 13125 The `alignment` parameter was added on September 27, 2021 13126 +/ 13127 this(string label, TextAlignment alignment = TextAlignment.Center) { 13128 this.label = label; 13129 this.displayFlags = DisplayFlags.displayText; 13130 this.alignment = alignment; 13131 } 13132 13133 /// ditto 13134 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 13135 this.label = label; 13136 this.image = image; 13137 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 13138 this.alignment = alignment; 13139 } 13140 13141 /// ditto 13142 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 13143 this.image = image; 13144 this.displayFlags = DisplayFlags.displayImage; 13145 this.alignment = alignment; 13146 } 13147 13148 /// ditto 13149 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 13150 this.label = label; 13151 this.image = image; 13152 this.alignment = alignment; 13153 this.displayFlags = displayFlags; 13154 } 13155 13156 string label; 13157 MemoryImage image; 13158 13159 enum DisplayFlags { 13160 displayText = 1 << 0, 13161 displayImage = 1 << 1, 13162 } 13163 13164 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 13165 13166 TextAlignment alignment; 13167 } 13168 13169 /++ 13170 A basic checked or not checked box with an attached label. 13171 13172 13173 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 13174 13175 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13176 13177 History: 13178 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. 13179 +/ 13180 class Checkbox : MouseActivatedWidget { 13181 version(win32_widgets) { 13182 override int maxHeight() { return scaleWithDpi(16); } 13183 override int minHeight() { return scaleWithDpi(16); } 13184 } else version(custom_widgets) { 13185 private enum buttonSize = 16; 13186 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 13187 override int minHeight() { return maxHeight(); } 13188 } else static assert(0); 13189 13190 override int marginLeft() { return 4; } 13191 13192 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 13193 13194 /++ 13195 Just an alias because I keep typing checked out of web habit. 13196 13197 History: 13198 Added May 31, 2021 13199 +/ 13200 alias checked = isChecked; 13201 13202 private string label; 13203 private dchar accelerator; 13204 13205 /++ 13206 +/ 13207 this(string label, Widget parent) { 13208 this(ImageLabel(label), Appearance.checkbox, parent); 13209 } 13210 13211 /// ditto 13212 this(string label, Appearance appearance, Widget parent) { 13213 this(ImageLabel(label), appearance, parent); 13214 } 13215 13216 /++ 13217 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 13218 13219 History: 13220 Added June 29, 2021 (dub v10.2) 13221 +/ 13222 enum Appearance { 13223 checkbox, /// a normal checkbox 13224 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 13225 //sliderswitch, 13226 } 13227 private Appearance appearance; 13228 13229 /// ditto 13230 private this(ImageLabel label, Appearance appearance, Widget parent) { 13231 super(parent); 13232 version(win32_widgets) { 13233 this.label = label.label; 13234 13235 uint extraStyle; 13236 final switch(appearance) { 13237 case Appearance.checkbox: 13238 break; 13239 case Appearance.pushbutton: 13240 extraStyle |= BS_PUSHLIKE; 13241 break; 13242 } 13243 13244 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 13245 } else version(custom_widgets) { 13246 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 13247 } else static assert(0); 13248 } 13249 13250 version(custom_widgets) 13251 override void paint(WidgetPainter painter) { 13252 auto cs = getComputedStyle(); 13253 if(isFocused()) { 13254 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 13255 painter.fillColor = cs.windowBackgroundColor; 13256 painter.drawRectangle(Point(0, 0), width, height); 13257 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 13258 } else { 13259 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 13260 painter.fillColor = cs.windowBackgroundColor; 13261 painter.drawRectangle(Point(0, 0), width, height); 13262 } 13263 13264 13265 painter.outlineColor = Color.black; 13266 painter.fillColor = Color.white; 13267 enum rectOffset = 2; 13268 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 13269 13270 if(isChecked) { 13271 auto size = scaleWithDpi(2); 13272 painter.pen = Pen(Color.black, size); 13273 // I'm using height so the checkbox is square 13274 enum padding = 3; 13275 painter.drawLine( 13276 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 13277 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 13278 ); 13279 painter.drawLine( 13280 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 13281 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 13282 ); 13283 13284 painter.pen = Pen(Color.black, 1); 13285 } 13286 13287 if(label !is null) { 13288 painter.outlineColor = cs.foregroundColor(); 13289 painter.fillColor = cs.foregroundColor(); 13290 13291 // i want the centerline of the text to be aligned with the centerline of the checkbox 13292 /+ 13293 auto font = cs.font(); 13294 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 13295 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 13296 +/ 13297 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 13298 } 13299 } 13300 13301 override void defaultEventHandler_triggered(Event ev) { 13302 isChecked = !isChecked; 13303 13304 this.emit!(ChangeEvent!bool)(&isChecked); 13305 13306 redraw(); 13307 } 13308 13309 /// Emits a change event with the checked state 13310 mixin Emits!(ChangeEvent!bool); 13311 } 13312 13313 /// Adds empty space to a layout. 13314 class VerticalSpacer : Widget { 13315 private int mh; 13316 13317 /++ 13318 History: 13319 The overload with `maxHeight` was added on December 31, 2024 13320 +/ 13321 this(Widget parent) { 13322 this(0, parent); 13323 } 13324 13325 /// ditto 13326 this(int maxHeight, Widget parent) { 13327 this.mh = maxHeight; 13328 super(parent); 13329 this.tabStop = false; 13330 } 13331 13332 override int maxHeight() { 13333 return mh ? scaleWithDpi(mh) : super.maxHeight(); 13334 } 13335 } 13336 13337 13338 /// ditto 13339 class HorizontalSpacer : Widget { 13340 private int mw; 13341 13342 /++ 13343 History: 13344 The overload with `maxWidth` was added on December 31, 2024 13345 +/ 13346 this(Widget parent) { 13347 this(0, parent); 13348 } 13349 13350 /// ditto 13351 this(int maxWidth, Widget parent) { 13352 this.mw = maxWidth; 13353 super(parent); 13354 this.tabStop = false; 13355 } 13356 13357 override int maxWidth() { 13358 return mw ? scaleWithDpi(mw) : super.maxWidth(); 13359 } 13360 } 13361 13362 13363 /++ 13364 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 13365 13366 13367 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 13368 13369 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13370 13371 History: 13372 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. 13373 +/ 13374 class Radiobox : MouseActivatedWidget { 13375 13376 version(win32_widgets) { 13377 override int maxHeight() { return scaleWithDpi(16); } 13378 override int minHeight() { return scaleWithDpi(16); } 13379 } else version(custom_widgets) { 13380 private enum buttonSize = 16; 13381 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 13382 override int minHeight() { return maxHeight(); } 13383 } else static assert(0); 13384 13385 override int marginLeft() { return 4; } 13386 13387 // FIXME: make a label getter 13388 private string label; 13389 private dchar accelerator; 13390 13391 /++ 13392 13393 +/ 13394 this(string label, Widget parent) { 13395 super(parent); 13396 version(win32_widgets) { 13397 this.label = label; 13398 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 13399 } else version(custom_widgets) { 13400 label.extractWindowsStyleLabel(this.label, this.accelerator); 13401 height = 16; 13402 width = height + 4 + cast(int) label.length * 16; 13403 } 13404 } 13405 13406 version(custom_widgets) 13407 override void paint(WidgetPainter painter) { 13408 auto cs = getComputedStyle(); 13409 13410 if(isFocused) { 13411 painter.fillColor = cs.windowBackgroundColor; 13412 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 13413 } else { 13414 painter.fillColor = cs.windowBackgroundColor; 13415 painter.outlineColor = cs.windowBackgroundColor; 13416 } 13417 painter.drawRectangle(Point(0, 0), width, height); 13418 13419 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 13420 13421 painter.outlineColor = Color.black; 13422 painter.fillColor = Color.white; 13423 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 13424 if(isChecked) { 13425 painter.outlineColor = Color.black; 13426 painter.fillColor = Color.black; 13427 // I'm using height so the checkbox is square 13428 auto size = scaleWithDpi(2); 13429 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 13430 } 13431 13432 painter.outlineColor = cs.foregroundColor(); 13433 painter.fillColor = cs.foregroundColor(); 13434 13435 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 13436 } 13437 13438 13439 override void defaultEventHandler_triggered(Event ev) { 13440 isChecked = true; 13441 13442 if(this.parent) { 13443 foreach(child; this.parent.children) { 13444 if(child is this) continue; 13445 if(auto rb = cast(Radiobox) child) { 13446 rb.isChecked = false; 13447 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 13448 rb.redraw(); 13449 } 13450 } 13451 } 13452 13453 this.emit!(ChangeEvent!bool)(&this.isChecked); 13454 13455 redraw(); 13456 } 13457 13458 /// 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. 13459 mixin Emits!(ChangeEvent!bool); 13460 } 13461 13462 13463 /++ 13464 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 13465 13466 13467 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 13468 13469 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13470 13471 History: 13472 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. 13473 +/ 13474 class Button : MouseActivatedWidget { 13475 override int heightStretchiness() { return 3; } 13476 override int widthStretchiness() { return 3; } 13477 13478 /++ 13479 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. 13480 13481 History: 13482 Added July 2, 2021 13483 +/ 13484 public bool triggersOnMultiClick; 13485 13486 private string label_; 13487 private TextAlignment alignment; 13488 private dchar accelerator; 13489 13490 /// 13491 string label() { return label_; } 13492 /// 13493 void label(string l) { 13494 label_ = l; 13495 version(win32_widgets) { 13496 WCharzBuffer bfr = WCharzBuffer(l); 13497 SetWindowTextW(hwnd, bfr.ptr); 13498 } else version(custom_widgets) { 13499 redraw(); 13500 } 13501 } 13502 13503 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 13504 super.defaultEventHandler_dblclick(ev); 13505 if(triggersOnMultiClick) { 13506 if(ev.button == MouseButton.left) { 13507 auto event = new Event(EventType.triggered, this); 13508 event.sendDirectly(); 13509 } 13510 } 13511 } 13512 13513 private Sprite sprite; 13514 private int displayFlags; 13515 13516 protected bool needsOwnerDraw() { 13517 return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent; 13518 } 13519 13520 version(win32_widgets) 13521 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) { 13522 auto itemId = dis.itemID; 13523 auto hdc = dis.hDC; 13524 auto rect = dis.rcItem; 13525 switch(dis.itemAction) { 13526 // skipping setDynamicState because i don't want to queue the redraw unnecessarily 13527 case ODA_SELECT: 13528 dynamicState_ &= ~DynamicState.depressed; 13529 if(dis.itemState & ODS_SELECTED) 13530 dynamicState_ |= DynamicState.depressed; 13531 goto case; 13532 case ODA_FOCUS: 13533 dynamicState_ &= ~DynamicState.focus; 13534 if(dis.itemState & ODS_FOCUS) 13535 dynamicState_ |= DynamicState.focus; 13536 goto case; 13537 case ODA_DRAWENTIRE: 13538 auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this); 13539 //painter.impl.hdc = hdc; 13540 paint(painter); 13541 break; 13542 default: 13543 } 13544 return 1; 13545 13546 } 13547 13548 /++ 13549 Creates a push button with the given label, which may be an image or some text. 13550 13551 Bugs: 13552 If the image is bigger than the button, it may not be displayed in the right position on Linux. 13553 13554 History: 13555 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 13556 13557 The button with label and image will respect requests to show both on Windows as 13558 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 13559 +/ 13560 this(string label, Widget parent) { 13561 this(ImageLabel(label), parent); 13562 } 13563 13564 /// ditto 13565 this(ImageLabel label, Widget parent) { 13566 bool needsImage; 13567 version(win32_widgets) { 13568 super(parent); 13569 13570 // BS_BITMAP is set when we want image only, so checking for exactly that combination 13571 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 13572 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 13573 13574 // could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check. 13575 13576 if(needsOwnerDraw) { 13577 extraStyle |= BS_OWNERDRAW; 13578 needsImage = true; 13579 } 13580 13581 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 13582 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 13583 13584 if(label.image) { 13585 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 13586 13587 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 13588 } 13589 13590 this.label = label.label; 13591 } else version(custom_widgets) { 13592 super(parent); 13593 13594 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 13595 needsImage = true; 13596 } 13597 13598 13599 if(needsImage && label.image) { 13600 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 13601 this.displayFlags = label.displayFlags; 13602 } 13603 13604 this.alignment = label.alignment; 13605 } 13606 13607 override int minHeight() { return defaultLineHeight + 4; } 13608 13609 static class Style : Widget.Style { 13610 override WidgetBackground background() { 13611 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 13612 13613 auto pressed = DynamicState.depressed | DynamicState.hover; 13614 if((widget.dynamicState & pressed) == pressed && widget.enabled) { 13615 return WidgetBackground(cs.depressedButtonColor()); 13616 } else if(widget.dynamicState & DynamicState.hover && widget.enabled) { 13617 return WidgetBackground(cs.hoveringColor()); 13618 } else { 13619 return WidgetBackground(cs.buttonColor()); 13620 } 13621 } 13622 13623 override Color foregroundColor() { 13624 auto clr = super.foregroundColor(); 13625 if(widget.enabled) return clr; 13626 13627 return Color(clr.r, clr.g, clr.b, clr.a / 2); 13628 } 13629 13630 override FrameStyle borderStyle() { 13631 auto pressed = DynamicState.depressed | DynamicState.hover; 13632 if((widget.dynamicState & pressed) == pressed && widget.enabled) { 13633 return FrameStyle.sunk; 13634 } else { 13635 return FrameStyle.risen; 13636 } 13637 13638 } 13639 13640 override bool variesWithState(ulong dynamicStateFlags) { 13641 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 13642 } 13643 } 13644 mixin OverrideStyle!Style; 13645 13646 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13647 if(sprite) { 13648 sprite.drawAt( 13649 painter, 13650 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 13651 Point(0, 0) 13652 ); 13653 } else { 13654 Point pos = bounds.upperLeft; 13655 if(this.height == 16) 13656 pos.y -= 2; // total hack omg 13657 13658 painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 13659 } 13660 return bounds; 13661 } 13662 13663 override int flexBasisWidth() { 13664 version(win32_widgets) { 13665 SIZE size; 13666 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13667 if(size.cx == 0) 13668 goto fallback; 13669 return size.cx + scaleWithDpi(16); 13670 } 13671 fallback: 13672 return scaleWithDpi(cast(int) label.length * 8 + 16); 13673 } 13674 13675 override int flexBasisHeight() { 13676 version(win32_widgets) { 13677 SIZE size; 13678 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13679 if(size.cy == 0) 13680 goto fallback; 13681 return size.cy + scaleWithDpi(6); 13682 } 13683 fallback: 13684 return defaultLineHeight + 4; 13685 } 13686 } 13687 13688 /++ 13689 A button with a custom appearance, even on systems where there is a standard button. You can subclass it to override its style, paint, or paintContent functions, or you can modify its members for common changes. 13690 13691 History: 13692 Added January 14, 2024 13693 +/ 13694 class CustomButton : Button { 13695 this(ImageLabel label, Widget parent) { 13696 super(label, parent); 13697 } 13698 13699 this(string label, Widget parent) { 13700 super(label, parent); 13701 } 13702 13703 version(win32_widgets) 13704 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 13705 // paint is driven by handleWmDrawItem instead of minigui's redraw events 13706 if(hwnd) 13707 InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw 13708 return; 13709 } 13710 13711 override void paint(WidgetPainter painter) { 13712 // the parent does `if(hwnd) return;` because 13713 // normally we don't want to draw on standard controls, 13714 // but this is an exception if it is an owner drawn button 13715 // (which is determined in the constructor by testing, 13716 // at runtime, for the existence of an overridden paint 13717 // member anyway, so this needed to trigger BS_OWNERDRAW) 13718 // sdpyPrintDebugString("drawing"); 13719 painter.drawThemed(&paintContent); 13720 } 13721 } 13722 13723 /++ 13724 A button with a consistent size, suitable for user commands like OK and CANCEL. 13725 +/ 13726 class CommandButton : Button { 13727 this(string label, Widget parent) { 13728 super(label, parent); 13729 } 13730 13731 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 13732 13733 override int maxHeight() { 13734 return defaultLineHeight + 4; 13735 } 13736 13737 override int maxWidth() { 13738 return defaultLineHeight * 4; 13739 } 13740 13741 override int marginLeft() { return 12; } 13742 override int marginRight() { return 12; } 13743 override int marginTop() { return 12; } 13744 override int marginBottom() { return 12; } 13745 } 13746 13747 /// 13748 enum ArrowDirection { 13749 left, /// 13750 right, /// 13751 up, /// 13752 down /// 13753 } 13754 13755 /// 13756 version(custom_widgets) 13757 class ArrowButton : Button { 13758 /// 13759 this(ArrowDirection direction, Widget parent) { 13760 super("", parent); 13761 this.direction = direction; 13762 triggersOnMultiClick = true; 13763 } 13764 13765 private ArrowDirection direction; 13766 13767 override int minHeight() { return scaleWithDpi(16); } 13768 override int maxHeight() { return scaleWithDpi(16); } 13769 override int minWidth() { return scaleWithDpi(16); } 13770 override int maxWidth() { return scaleWithDpi(16); } 13771 13772 override void paint(WidgetPainter painter) { 13773 super.paint(painter); 13774 13775 auto cs = getComputedStyle(); 13776 13777 painter.outlineColor = cs.foregroundColor; 13778 painter.fillColor = cs.foregroundColor; 13779 13780 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 13781 13782 final switch(direction) { 13783 case ArrowDirection.up: 13784 painter.drawPolygon( 13785 scaleWithDpi(Point(2, 10) + offset), 13786 scaleWithDpi(Point(7, 5) + offset), 13787 scaleWithDpi(Point(12, 10) + offset), 13788 scaleWithDpi(Point(2, 10) + offset) 13789 ); 13790 break; 13791 case ArrowDirection.down: 13792 painter.drawPolygon( 13793 scaleWithDpi(Point(2, 6) + offset), 13794 scaleWithDpi(Point(7, 11) + offset), 13795 scaleWithDpi(Point(12, 6) + offset), 13796 scaleWithDpi(Point(2, 6) + offset) 13797 ); 13798 break; 13799 case ArrowDirection.left: 13800 painter.drawPolygon( 13801 scaleWithDpi(Point(10, 2) + offset), 13802 scaleWithDpi(Point(5, 7) + offset), 13803 scaleWithDpi(Point(10, 12) + offset), 13804 scaleWithDpi(Point(10, 2) + offset) 13805 ); 13806 break; 13807 case ArrowDirection.right: 13808 painter.drawPolygon( 13809 scaleWithDpi(Point(6, 2) + offset), 13810 scaleWithDpi(Point(11, 7) + offset), 13811 scaleWithDpi(Point(6, 12) + offset), 13812 scaleWithDpi(Point(6, 2) + offset) 13813 ); 13814 break; 13815 } 13816 } 13817 } 13818 13819 private 13820 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 13821 int x, y; 13822 Widget par = c; 13823 while(par) { 13824 x += par.x; 13825 y += par.y; 13826 par = par.parent; 13827 } 13828 return [x, y]; 13829 } 13830 13831 version(win32_widgets) 13832 private 13833 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 13834 // MapWindowPoints? 13835 int x, y; 13836 Widget par = c; 13837 while(par) { 13838 x += par.x; 13839 y += par.y; 13840 par = par.parent; 13841 if(par !is null && par.useNativeDrawing()) 13842 break; 13843 } 13844 return [x, y]; 13845 } 13846 13847 /// 13848 class ImageBox : Widget { 13849 private MemoryImage image_; 13850 13851 override int widthStretchiness() { return 1; } 13852 override int heightStretchiness() { return 1; } 13853 override int widthShrinkiness() { return 1; } 13854 override int heightShrinkiness() { return 1; } 13855 13856 override int flexBasisHeight() { 13857 return image_.height; 13858 } 13859 13860 override int flexBasisWidth() { 13861 return image_.width; 13862 } 13863 13864 /// 13865 public void setImage(MemoryImage image){ 13866 this.image_ = image; 13867 if(this.parentWindow && this.parentWindow.win) { 13868 if(sprite) 13869 sprite.dispose(); 13870 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13871 } 13872 redraw(); 13873 } 13874 13875 /// How to fit the image in the box if they aren't an exact match in size? 13876 enum HowToFit { 13877 center, /// centers the image, cropping around all the edges as needed 13878 crop, /// always draws the image in the upper left, cropping the lower right if needed 13879 // stretch, /// not implemented 13880 } 13881 13882 private Sprite sprite; 13883 private HowToFit howToFit_; 13884 13885 private Color backgroundColor_; 13886 13887 /// 13888 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 13889 this.image_ = image; 13890 this.tabStop = false; 13891 this.howToFit_ = howToFit; 13892 this.backgroundColor_ = backgroundColor; 13893 super(parent); 13894 updateSprite(); 13895 } 13896 13897 /// ditto 13898 this(MemoryImage image, HowToFit howToFit, Widget parent) { 13899 this(image, howToFit, Color.transparent, parent); 13900 } 13901 13902 private void updateSprite() { 13903 if(sprite is null && this.parentWindow && this.parentWindow.win) { 13904 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13905 } 13906 } 13907 13908 override void paint(WidgetPainter painter) { 13909 updateSprite(); 13910 if(backgroundColor_.a) { 13911 painter.fillColor = backgroundColor_; 13912 painter.drawRectangle(Point(0, 0), width, height); 13913 } 13914 if(howToFit_ == HowToFit.crop) 13915 sprite.drawAt(painter, Point(0, 0)); 13916 else if(howToFit_ == HowToFit.center) { 13917 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 13918 } 13919 } 13920 } 13921 13922 /// 13923 class TextLabel : Widget { 13924 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 13925 override int maxHeight() { return minHeight; } 13926 override int minWidth() { return 32; } 13927 13928 override int flexBasisHeight() { return minHeight(); } 13929 override int flexBasisWidth() { return defaultTextWidth(label); } 13930 13931 string label_; 13932 13933 /++ 13934 Indicates which other control this label is here for. Similar to HTML `for` attribute. 13935 13936 In practice this means a click on the label will focus the `labelFor`. In future versions 13937 it will also set screen reader hints but that is not yet implemented. 13938 13939 History: 13940 Added October 3, 2021 (dub v10.4) 13941 +/ 13942 Widget labelFor; 13943 13944 /// 13945 @scriptable 13946 string label() { return label_; } 13947 13948 /// 13949 @scriptable 13950 void label(string l) { 13951 label_ = l; 13952 version(win32_widgets) { 13953 WCharzBuffer bfr = WCharzBuffer(l); 13954 SetWindowTextW(hwnd, bfr.ptr); 13955 } else version(custom_widgets) 13956 redraw(); 13957 } 13958 13959 override void defaultEventHandler_click(scope ClickEvent ce) { 13960 if(this.labelFor !is null) 13961 this.labelFor.focus(); 13962 } 13963 13964 /++ 13965 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 13966 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 13967 +/ 13968 this(string label, TextAlignment alignment, Widget parent) { 13969 this.label_ = label; 13970 this.alignment = alignment; 13971 this.tabStop = false; 13972 super(parent); 13973 13974 version(win32_widgets) 13975 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 13976 } 13977 13978 /// ditto 13979 this(string label, Widget parent) { 13980 this(label, TextAlignment.Right, parent); 13981 } 13982 13983 TextAlignment alignment; 13984 13985 version(custom_widgets) 13986 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13987 painter.outlineColor = getComputedStyle().foregroundColor; 13988 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 13989 return bounds; 13990 } 13991 } 13992 13993 class TextDisplayHelper : Widget { 13994 protected TextLayouter l; 13995 protected ScrollMessageWidget smw; 13996 13997 private const(TextLayouter.State)*[] undoStack; 13998 private const(TextLayouter.State)*[] redoStack; 13999 14000 private string preservedPrimaryText; 14001 protected void selectionChanged() { 14002 // sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString()); 14003 static if(UsingSimpledisplayX11) 14004 with(l.selection()) { 14005 if(!isEmpty()) { 14006 //sdpyPrintDebugString("!isEmpty"); 14007 14008 getPrimarySelection(parentWindow.win, (in char[] txt) { 14009 // sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")"); 14010 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 14011 if(txt.length) { 14012 preservedPrimaryText = txt.idup; 14013 // writeln(preservedPrimaryText); 14014 } 14015 14016 setPrimarySelection(parentWindow.win, getContentString()); 14017 }); 14018 } 14019 } 14020 } 14021 14022 final TextLayouter layouter() { 14023 return l; 14024 } 14025 14026 bool readonly; 14027 bool caretNavigation; // scroll lock can flip this 14028 bool singleLine; 14029 bool acceptsTabInput; 14030 14031 private Menu ctx; 14032 override Menu contextMenu(int x, int y) { 14033 if(ctx is null) { 14034 ctx = new Menu("Actions", this); 14035 if(!readonly) { 14036 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 14037 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 14038 ctx.addSeparator(); 14039 } 14040 if(!readonly) 14041 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 14042 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 14043 if(!readonly) 14044 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 14045 if(!readonly) 14046 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 14047 ctx.addSeparator(); 14048 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 14049 } 14050 return ctx; 14051 } 14052 14053 override void defaultEventHandler_blur(BlurEvent ev) { 14054 super.defaultEventHandler_blur(ev); 14055 if(l.wasMutated()) { 14056 auto evt = new ChangeEvent!string(this, &this.content); 14057 evt.dispatch(); 14058 l.clearWasMutatedFlag(); 14059 } 14060 } 14061 14062 private string content() { 14063 return l.getTextString(); 14064 } 14065 14066 void undo() { 14067 if(readonly) return; 14068 if(undoStack.length) { 14069 auto state = undoStack[$-1]; 14070 undoStack = undoStack[0 .. $-1]; 14071 undoStack.assumeSafeAppend(); 14072 redoStack ~= l.saveState(); 14073 l.restoreState(state); 14074 adjustScrollbarSizes(); 14075 scrollForCaret(); 14076 redraw(); 14077 stateCheckpoint = true; 14078 } 14079 } 14080 14081 void redo() { 14082 if(readonly) return; 14083 if(redoStack.length) { 14084 doStateCheckpoint(); 14085 auto state = redoStack[$-1]; 14086 redoStack = redoStack[0 .. $-1]; 14087 redoStack.assumeSafeAppend(); 14088 l.restoreState(state); 14089 adjustScrollbarSizes(); 14090 scrollForCaret(); 14091 redraw(); 14092 stateCheckpoint = true; 14093 } 14094 } 14095 14096 void cut() { 14097 if(readonly) return; 14098 with(l.selection()) { 14099 if(!isEmpty()) { 14100 setClipboardText(parentWindow.win, getContentString()); 14101 doStateCheckpoint(); 14102 replaceContent(""); 14103 adjustScrollbarSizes(); 14104 scrollForCaret(); 14105 this.redraw(); 14106 } 14107 } 14108 14109 } 14110 14111 void copy() { 14112 with(l.selection()) { 14113 if(!isEmpty()) { 14114 setClipboardText(parentWindow.win, getContentString()); 14115 this.redraw(); 14116 } 14117 } 14118 } 14119 14120 void paste() { 14121 if(readonly) return; 14122 getClipboardText(parentWindow.win, (txt) { 14123 doStateCheckpoint(); 14124 if(singleLine) 14125 l.selection.replaceContent(txt.stripInternal()); 14126 else 14127 l.selection.replaceContent(txt); 14128 adjustScrollbarSizes(); 14129 scrollForCaret(); 14130 this.redraw(); 14131 }); 14132 } 14133 14134 void deleteContentOfSelection() { 14135 if(readonly) return; 14136 doStateCheckpoint(); 14137 l.selection.replaceContent(""); 14138 l.selection.setUserXCoordinate(); 14139 adjustScrollbarSizes(); 14140 scrollForCaret(); 14141 redraw(); 14142 } 14143 14144 void selectAll() { 14145 with(l.selection) { 14146 moveToStartOfDocument(); 14147 setAnchor(); 14148 moveToEndOfDocument(); 14149 setFocus(); 14150 14151 selectionChanged(); 14152 } 14153 redraw(); 14154 } 14155 14156 protected bool stateCheckpoint = true; 14157 14158 protected void doStateCheckpoint() { 14159 if(stateCheckpoint) { 14160 undoStack ~= l.saveState(); 14161 stateCheckpoint = false; 14162 } 14163 } 14164 14165 protected void adjustScrollbarSizes() { 14166 // FIXME: will want a content area helper function instead of doing all these subtractions myself 14167 auto borderWidth = 2; 14168 this.smw.setTotalArea(l.width, l.height); 14169 this.smw.setViewableArea( 14170 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 14171 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 14172 } 14173 14174 protected void scrollForCaret() { 14175 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 14176 smw.scrollIntoView(l.selection.focusBoundingBox()); 14177 } 14178 14179 // FIXME: this should be a theme changed event listener instead 14180 private BaseVisualTheme currentTheme; 14181 override void recomputeChildLayout() { 14182 if(currentTheme is null) 14183 currentTheme = WidgetPainter.visualTheme; 14184 if(WidgetPainter.visualTheme !is currentTheme) { 14185 currentTheme = WidgetPainter.visualTheme; 14186 auto ds = this.l.defaultStyle; 14187 if(auto ms = cast(MyTextStyle) ds) { 14188 auto cs = getComputedStyle(); 14189 auto font = cs.font(); 14190 if(font !is null) 14191 ms.font_ = font; 14192 else { 14193 auto osc = new OperatingSystemFont(); 14194 osc.loadDefault; 14195 ms.font_ = osc; 14196 } 14197 } 14198 } 14199 super.recomputeChildLayout(); 14200 } 14201 14202 private Point adjustForSingleLine(Point p) { 14203 if(singleLine) 14204 return Point(p.x, this.height / 2); 14205 else 14206 return p; 14207 } 14208 14209 private bool wordWrapEnabled_; 14210 14211 this(TextLayouter l, ScrollMessageWidget parent) { 14212 this.smw = parent; 14213 14214 smw.addDefaultWheelListeners(16, 16, 8); 14215 smw.movementPerButtonClick(16, 16); 14216 14217 this.defaultPadding = Rectangle(2, 2, 2, 2); 14218 14219 this.l = l; 14220 super(parent); 14221 14222 smw.addEventListener((scope ScrollEvent se) { 14223 this.redraw(); 14224 }); 14225 14226 this.addEventListener((scope ResizeEvent re) { 14227 // FIXME: I should add a method to give this client area width thing 14228 if(wordWrapEnabled_) 14229 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 14230 14231 adjustScrollbarSizes(); 14232 scrollForCaret(); 14233 14234 this.redraw(); 14235 }); 14236 14237 } 14238 14239 private { 14240 bool mouseDown; 14241 bool mouseActuallyMoved; 14242 14243 Point downAt; 14244 14245 Timer autoscrollTimer; 14246 int autoscrollDirection; 14247 int autoscrollAmount; 14248 14249 void autoscroll() { 14250 switch(autoscrollDirection) { 14251 case 0: smw.scrollUp(autoscrollAmount); break; 14252 case 1: smw.scrollDown(autoscrollAmount); break; 14253 case 2: smw.scrollLeft(autoscrollAmount); break; 14254 case 3: smw.scrollRight(autoscrollAmount); break; 14255 default: assert(0); 14256 } 14257 14258 this.redraw(); 14259 } 14260 14261 void setAutoscrollTimer(int direction, int amount) { 14262 if(autoscrollTimer is null) { 14263 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 14264 } 14265 14266 autoscrollDirection = direction; 14267 autoscrollAmount = amount; 14268 } 14269 14270 void stopAutoscrollTimer() { 14271 if(autoscrollTimer !is null) { 14272 autoscrollTimer.dispose(); 14273 autoscrollTimer = null; 14274 } 14275 autoscrollAmount = 0; 14276 autoscrollDirection = 0; 14277 } 14278 } 14279 14280 override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) { 14281 if(mouseDown) { 14282 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 14283 14284 // FIXME: when scrolling i actually do want a timer. 14285 // i also want a zone near the sides of the window where i can auto scroll 14286 14287 auto scrollMultiplier = scaleWithDpi(16); 14288 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 14289 14290 if(!singleLine && movedTo.y < 4) { 14291 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 14292 } else 14293 if(!singleLine && (movedTo.y + 6) > this.height) { 14294 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 14295 } else 14296 if(movedTo.x < 4) { 14297 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 14298 } else 14299 if((movedTo.x + 6) > this.width) { 14300 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 14301 } else 14302 stopAutoscrollTimer(); 14303 14304 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 14305 l.selection.setFocus(); 14306 mouseActuallyMoved = true; 14307 this.redraw(); 14308 } 14309 14310 super.defaultEventHandler_mousemove(ce); 14311 } 14312 14313 override void defaultEventHandler_mouseup(scope MouseUpEvent ce) { 14314 // FIXME: assert primary selection 14315 if(mouseDown && ce.button == MouseButton.left) { 14316 stateCheckpoint = true; 14317 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 14318 //l.selection.setFocus(); 14319 mouseDown = false; 14320 parentWindow.releaseMouseCapture(); 14321 stopAutoscrollTimer(); 14322 this.redraw(); 14323 14324 if(mouseActuallyMoved) 14325 selectionChanged(); 14326 } 14327 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 14328 14329 super.defaultEventHandler_mouseup(ce); 14330 } 14331 14332 static if(UsingSimpledisplayX11) 14333 override void defaultEventHandler_click(scope ClickEvent ce) { 14334 if(ce.button == MouseButton.middle) { 14335 parentWindow.win.getPrimarySelection((txt) { 14336 doStateCheckpoint(); 14337 14338 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 14339 14340 if(txt == l.selection.getContentString && preservedPrimaryText.length) 14341 l.selection.replaceContent(preservedPrimaryText); 14342 else 14343 l.selection.replaceContent(txt); 14344 redraw(); 14345 }); 14346 } 14347 14348 super.defaultEventHandler_click(ce); 14349 } 14350 14351 override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) { 14352 if(dce.button == MouseButton.left) { 14353 with(l.selection()) { 14354 // FIXME: for a url or file picker i might wanna use / as a separator intead 14355 scope dg = delegate const(char)[] (scope return const(char)[] ch) { 14356 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 14357 return ch; 14358 return null; 14359 }; 14360 find(dg, 1, true).moveToEnd.setAnchor; 14361 find(dg, 1, false).moveTo.setFocus; 14362 selectionChanged(); 14363 redraw(); 14364 } 14365 } 14366 14367 super.defaultEventHandler_dblclick(dce); 14368 } 14369 14370 override void defaultEventHandler_mousedown(scope MouseDownEvent ce) { 14371 if(ce.button == MouseButton.left) { 14372 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 14373 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 14374 if(ce.shiftKey) 14375 l.selection.setFocus(); 14376 else 14377 l.selection.setAnchor(); 14378 mouseDown = true; 14379 mouseActuallyMoved = false; 14380 parentWindow.captureMouse(this); 14381 this.redraw(); 14382 } 14383 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 14384 14385 super.defaultEventHandler_mousedown(ce); 14386 } 14387 14388 override void defaultEventHandler_char(scope CharEvent ce) { 14389 super.defaultEventHandler_char(ce); 14390 14391 if(readonly) 14392 return; 14393 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 14394 return; // skip the ctrl+x characters we don't care about as plain text 14395 14396 if(singleLine && ce.character == '\n') 14397 return; 14398 if(!acceptsTabInput && ce.character == '\t') 14399 return; 14400 14401 doStateCheckpoint(); 14402 14403 char[4] buffer; 14404 import arsd.core; 14405 auto stride = encodeUtf8(buffer, ce.character); 14406 l.selection.replaceContent(buffer[0 .. stride]); 14407 l.selection.setUserXCoordinate(); 14408 adjustScrollbarSizes(); 14409 scrollForCaret(); 14410 redraw(); 14411 14412 } 14413 14414 override void defaultEventHandler_keydown(scope KeyDownEvent kde) { 14415 switch(kde.key) { 14416 case Key.Up, Key.Down, Key.Left, Key.Right: 14417 case Key.Home, Key.End: 14418 stateCheckpoint = true; 14419 bool setPosition = false; 14420 switch(kde.key) { 14421 case Key.Up: l.selection.moveUp(); break; 14422 case Key.Down: l.selection.moveDown(); break; 14423 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 14424 case Key.Right: l.selection.moveRight(); setPosition = true; break; 14425 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 14426 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 14427 default: assert(0); 14428 } 14429 14430 if(kde.shiftKey) 14431 l.selection.setFocus(); 14432 else 14433 l.selection.setAnchor(); 14434 14435 selectionChanged(); 14436 14437 if(setPosition) 14438 l.selection.setUserXCoordinate(); 14439 scrollForCaret(); 14440 redraw(); 14441 break; 14442 case Key.PageUp, Key.PageDown: 14443 // want to act like the user clicked on the caret again 14444 // after the scroll operation completed, so it would remain at 14445 // about the same place on the viewport 14446 auto oldY = smw.vsb.position; 14447 smw.defaultKeyboardListener(kde); 14448 auto newY = smw.vsb.position; 14449 with(l.selection) { 14450 auto uc = getUserCoordinate(); 14451 uc.y += newY - oldY; 14452 moveTo(uc); 14453 14454 if(kde.shiftKey) 14455 setFocus(); 14456 else 14457 setAnchor(); 14458 } 14459 break; 14460 case Key.Delete: 14461 if(l.selection.isEmpty()) { 14462 l.selection.setAnchor(); 14463 l.selection.moveRight(); 14464 l.selection.setFocus(); 14465 } 14466 deleteContentOfSelection(); 14467 adjustScrollbarSizes(); 14468 scrollForCaret(); 14469 break; 14470 case Key.Insert: 14471 break; 14472 case Key.A: 14473 if(kde.ctrlKey) 14474 selectAll(); 14475 break; 14476 case Key.F: 14477 // find 14478 break; 14479 case Key.Z: 14480 if(kde.ctrlKey) 14481 undo(); 14482 break; 14483 case Key.R: 14484 if(kde.ctrlKey) 14485 redo(); 14486 break; 14487 case Key.X: 14488 if(kde.ctrlKey) 14489 cut(); 14490 break; 14491 case Key.C: 14492 if(kde.ctrlKey) 14493 copy(); 14494 break; 14495 case Key.V: 14496 if(kde.ctrlKey) 14497 paste(); 14498 break; 14499 case Key.F1: 14500 with(l.selection()) { 14501 moveToStartOfLine(); 14502 setAnchor(); 14503 moveToEndOfLine(); 14504 moveToIncludeAdjacentEndOfLineMarker(); 14505 setFocus(); 14506 replaceContent(""); 14507 } 14508 14509 redraw(); 14510 break; 14511 /* 14512 case Key.F2: 14513 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 14514 //(cast(MyTextStyle) old).font, 14515 font2, 14516 Color.red))); 14517 redraw(); 14518 break; 14519 */ 14520 case Key.Tab: 14521 // we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl 14522 if(acceptsTabInput && !kde.ctrlKey) 14523 kde.preventDefault(); 14524 break; 14525 default: 14526 } 14527 14528 if(!kde.defaultPrevented) 14529 super.defaultEventHandler_keydown(kde); 14530 } 14531 14532 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 14533 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 14534 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent 14535 if(parent && parent.parent && parent.parent.parent) 14536 parent.parent.parent.useStyleProperties(dg); 14537 else 14538 super.useStyleProperties(dg); 14539 } 14540 14541 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 14542 override int maxHeight() { 14543 if(singleLine) 14544 return minHeight; 14545 else 14546 return super.maxHeight(); 14547 } 14548 14549 void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14550 painter.setFont(myStyle.font); 14551 painter.drawText(upperLeft, text); 14552 } 14553 14554 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 14555 //painter.setFont(font); 14556 14557 auto cs = getComputedStyle(); 14558 auto defaultColor = cs.foregroundColor; 14559 14560 auto old = painter.setClipRectangleForWidget(bounds.upperLeft, bounds.width, bounds.height); 14561 scope(exit) painter.setClipRectangleForWidget(old.upperLeft, old.width, old.height); 14562 14563 l.getDrawableText(delegate bool(txt, style, info, carets...) { 14564 //writeln("Segment: ", txt); 14565 assert(style !is null); 14566 14567 if(info.selections && info.boundingBox.width > 0) { 14568 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 14569 painter.fillColor = color; 14570 painter.outlineColor = color; 14571 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 14572 painter.outlineColor = cs.selectionForegroundColor; 14573 //painter.fillColor = Color.white; 14574 } else { 14575 painter.outlineColor = defaultColor; 14576 } 14577 14578 if(this.isFocused) 14579 foreach(idx, caret; carets) { 14580 if(idx == 0) 14581 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 14582 painter.drawLine( 14583 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 14584 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 14585 ); 14586 } 14587 14588 if(txt.stripInternal.length) { 14589 // defaultColor = myStyle.color; // FIXME: so wrong 14590 if(auto myStyle = cast(MyTextStyle) style) 14591 drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 14592 else if(auto myStyle = cast(MyImageStyle) style) 14593 myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 14594 } 14595 14596 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 14597 return false; 14598 } else { 14599 return true; 14600 } 14601 }, Rectangle(smw.position(), bounds.size)); 14602 14603 /+ 14604 int place = 0; 14605 int y = 75; 14606 foreach(width; widths) { 14607 painter.fillColor = Color.red; 14608 painter.drawRectangle(Point(place, y), Size(width, 75)); 14609 //y += 15; 14610 place += width; 14611 } 14612 +/ 14613 14614 return bounds; 14615 } 14616 14617 static class MyTextStyle : TextStyle { 14618 OperatingSystemFont font_; 14619 this(OperatingSystemFont font, bool passwordMode = false) { 14620 this.font_ = font; 14621 } 14622 14623 override OperatingSystemFont font() { 14624 return font_; 14625 } 14626 14627 bool foregroundColorOverridden; 14628 bool backgroundColorOverridden; 14629 Color foregroundColor; 14630 Color backgroundColor; // should this be inline segment or the whole paragraph block? 14631 bool italic; 14632 bool bold; 14633 bool underline; 14634 bool strikeout; 14635 bool subscript; 14636 bool superscript; 14637 } 14638 14639 static class MyImageStyle : TextStyle, MeasurableFont { 14640 MemoryImage image_; 14641 Image converted; 14642 this(MemoryImage image) { 14643 this.image_ = image; 14644 this.converted = Image.fromMemoryImage(image); 14645 } 14646 14647 bool isMonospace() { return false; } 14648 fnum averageWidth() { return image_.width; } 14649 fnum height() { return image_.height; } 14650 fnum ascent() { return image_.height; } 14651 fnum descent() { return 0; } 14652 14653 fnum stringWidth(scope const(char)[] s, SimpleWindow window = null) { 14654 return image_.width; 14655 } 14656 14657 override MeasurableFont font() { 14658 return this; 14659 } 14660 14661 void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14662 painter.drawImage(upperLeft, converted); 14663 } 14664 } 14665 } 14666 14667 /+ 14668 class TextWidget : Widget { 14669 TextLayouter l; 14670 ScrollMessageWidget smw; 14671 TextDisplayHelper helper; 14672 this(TextLayouter l, Widget parent) { 14673 this.l = l; 14674 super(parent); 14675 14676 smw = new ScrollMessageWidget(this); 14677 //smw.horizontalScrollBar.hide; 14678 //smw.verticalScrollBar.hide; 14679 smw.addDefaultWheelListeners(16, 16, 8); 14680 smw.movementPerButtonClick(16, 16); 14681 helper = new TextDisplayHelper(l, smw); 14682 14683 // no need to do this here since there's gonna be a resize 14684 // event immediately before any drawing 14685 // smw.setTotalArea(l.width, l.height); 14686 smw.setViewableArea( 14687 this.width - this.paddingLeft - this.paddingRight, 14688 this.height - this.paddingTop - this.paddingBottom); 14689 14690 /+ 14691 writeln(l.width, "x", l.height); 14692 +/ 14693 } 14694 } 14695 +/ 14696 14697 14698 14699 14700 /+ 14701 make sure it calls parentWindow.inputProxy.setIMEPopupLocation too 14702 +/ 14703 14704 /++ 14705 Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit]. 14706 +/ 14707 abstract class EditableTextWidget : Widget { 14708 protected this(Widget parent) { 14709 version(custom_widgets) 14710 this(true, parent); 14711 else 14712 this(false, parent); 14713 } 14714 14715 private bool useCustomWidget; 14716 14717 protected this(bool useCustomWidget, Widget parent) { 14718 this.useCustomWidget = useCustomWidget; 14719 14720 super(parent); 14721 14722 if(useCustomWidget) 14723 setupCustomTextEditing(); 14724 } 14725 14726 private bool wordWrapEnabled_; 14727 /++ 14728 Enables or disables wrapping of long lines on word boundaries. 14729 +/ 14730 void wordWrapEnabled(bool enabled) { 14731 if(useCustomWidget) { 14732 wordWrapEnabled_ = enabled; 14733 if(tdh) 14734 tdh.wordWrapEnabled_ = true; 14735 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 14736 } else version(win32_widgets) { 14737 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 14738 } 14739 } 14740 14741 override int minWidth() { return scaleWithDpi(16); } 14742 override int widthStretchiness() { return 7; } 14743 override int widthShrinkiness() { return 1; } 14744 14745 override int maxHeight() { 14746 if(useCustomWidget) 14747 return tdh.maxHeight; 14748 else 14749 return super.maxHeight(); 14750 } 14751 14752 override void focus() { 14753 if(useCustomWidget && tdh) 14754 tdh.focus(); 14755 else 14756 super.focus(); 14757 } 14758 14759 override void defaultEventHandler_focusout(FocusOutEvent foe) { 14760 if(tdh !is null && foe.target is tdh) 14761 tdh.redraw(); 14762 } 14763 14764 override void defaultEventHandler_focusin(FocusInEvent foe) { 14765 if(tdh !is null && foe.target is tdh) 14766 tdh.redraw(); 14767 } 14768 14769 14770 /++ 14771 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. 14772 +/ 14773 void selectAll() { 14774 if(useCustomWidget) { 14775 tdh.selectAll(); 14776 } else version(win32_widgets) { 14777 SendMessage(hwnd, EM_SETSEL, 0, -1); 14778 } 14779 } 14780 14781 /++ 14782 Basic clipboard operations. 14783 14784 History: 14785 Added December 31, 2024 14786 +/ 14787 void copy() { 14788 if(useCustomWidget) { 14789 tdh.copy(); 14790 } else version(win32_widgets) { 14791 SendMessage(hwnd, WM_COPY, 0, 0); 14792 } 14793 } 14794 14795 /// ditto 14796 void cut() { 14797 if(useCustomWidget) { 14798 tdh.cut(); 14799 } else version(win32_widgets) { 14800 SendMessage(hwnd, WM_CUT, 0, 0); 14801 } 14802 } 14803 14804 /// ditto 14805 void paste() { 14806 if(useCustomWidget) { 14807 tdh.paste(); 14808 } else version(win32_widgets) { 14809 SendMessage(hwnd, WM_PASTE, 0, 0); 14810 } 14811 } 14812 14813 /// 14814 void undo() { 14815 if(useCustomWidget) { 14816 tdh.undo(); 14817 } else version(win32_widgets) { 14818 SendMessage(hwnd, EM_UNDO, 0, 0); 14819 } 14820 } 14821 14822 // note that WM_CLEAR deletes the selection without copying it to the clipboard 14823 // also windows supports margins, modified flag, and much more 14824 14825 // EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here 14826 14827 // EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc. 14828 14829 14830 14831 /*protected*/ TextDisplayHelper tdh; 14832 /*protected*/ TextLayouter textLayout; 14833 14834 /++ 14835 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. 14836 +/ 14837 @property string content() { 14838 if(useCustomWidget) { 14839 return textLayout.getTextString(); 14840 } else version(win32_widgets) { 14841 wchar[4096] bufferstack; 14842 wchar[] buffer; 14843 auto len = GetWindowTextLength(hwnd); 14844 if(len < bufferstack.length) 14845 buffer = bufferstack[0 .. len + 1]; 14846 else 14847 buffer = new wchar[](len + 1); 14848 14849 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 14850 if(l >= 0) 14851 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 14852 else 14853 return null; 14854 } 14855 14856 assert(0); 14857 } 14858 /// ditto 14859 @property void content(string s) { 14860 if(useCustomWidget) { 14861 with(textLayout.selection) { 14862 moveToStartOfDocument(); 14863 setAnchor(); 14864 moveToEndOfDocument(); 14865 setFocus(); 14866 replaceContent(s); 14867 } 14868 14869 tdh.adjustScrollbarSizes(); 14870 // these don't seem to help 14871 // tdh.smw.setPosition(0, 0); 14872 // tdh.scrollForCaret(); 14873 14874 redraw(); 14875 } else version(win32_widgets) { 14876 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 14877 SetWindowTextW(hwnd, bfr.ptr); 14878 } 14879 } 14880 14881 /++ 14882 Appends some text to the widget at the end, without affecting the user selection or cursor position. 14883 +/ 14884 void addText(string txt) { 14885 if(useCustomWidget) { 14886 textLayout.appendText(txt); 14887 tdh.adjustScrollbarSizes(); 14888 redraw(); 14889 } else version(win32_widgets) { 14890 // get the current selection 14891 DWORD StartPos, EndPos; 14892 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 14893 14894 // move the caret to the end of the text 14895 int outLength = GetWindowTextLengthW(hwnd); 14896 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 14897 14898 // insert the text at the new caret position 14899 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 14900 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 14901 14902 // restore the previous selection 14903 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 14904 } 14905 } 14906 14907 // EM_SCROLLCARET scrolls the caret into view 14908 14909 void scrollToBottom() { 14910 if(useCustomWidget) { 14911 tdh.smw.scrollDown(int.max); 14912 } else version(win32_widgets) { 14913 SendMessageW( hwnd, EM_LINESCROLL, 0, int.max ); 14914 } 14915 } 14916 14917 protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14918 return new TextDisplayHelper(textLayout, smw); 14919 } 14920 14921 protected TextStyle defaultTextStyle() { 14922 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 14923 } 14924 14925 private OperatingSystemFont getUsedFont() { 14926 auto cs = getComputedStyle(); 14927 auto font = cs.font; 14928 if(font is null) { 14929 font = new OperatingSystemFont; 14930 font.loadDefault(); 14931 } 14932 return font; 14933 } 14934 14935 protected void setupCustomTextEditing() { 14936 textLayout = new TextLayouter(defaultTextStyle()); 14937 14938 auto smw = new ScrollMessageWidget(this); 14939 if(!showingHorizontalScroll) 14940 smw.horizontalScrollBar.hide(); 14941 if(!showingVerticalScroll) 14942 smw.verticalScrollBar.hide(); 14943 this.tabStop = false; 14944 smw.tabStop = false; 14945 tdh = textDisplayHelperFactory(textLayout, smw); 14946 } 14947 14948 override void newParentWindow(Window old, Window n) { 14949 if(n is null) return; 14950 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 14951 if(textLayout) { 14952 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 14953 // the dpi change can change the font, so this informs the layouter that it has changed too 14954 style.font_ = getUsedFont(); 14955 14956 // arsd.core.writeln(this.parentWindow.win.actualDpi); 14957 } 14958 } 14959 }); 14960 } 14961 14962 static class Style : Widget.Style { 14963 override WidgetBackground background() { 14964 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 14965 } 14966 14967 override Color foregroundColor() { 14968 return WidgetPainter.visualTheme.foregroundColor; 14969 } 14970 14971 override FrameStyle borderStyle() { 14972 return FrameStyle.sunk; 14973 } 14974 14975 override MouseCursor cursor() { 14976 return GenericCursor.Text; 14977 } 14978 } 14979 mixin OverrideStyle!Style; 14980 14981 version(win32_widgets) { 14982 private string lastContentBlur; 14983 14984 override void defaultEventHandler_blur(BlurEvent ev) { 14985 super.defaultEventHandler_blur(ev); 14986 14987 if(!useCustomWidget) 14988 if(this.content != lastContentBlur) { 14989 auto evt = new ChangeEvent!string(this, &this.content); 14990 evt.dispatch(); 14991 lastContentBlur = this.content; 14992 } 14993 } 14994 } 14995 14996 14997 bool showingVerticalScroll() { return true; } 14998 bool showingHorizontalScroll() { return true; } 14999 } 15000 15001 /++ 15002 A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`. 15003 15004 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. 15005 15006 See_Also: 15007 [PasswordEdit] for a `LineEdit` that obscures its input. 15008 15009 [TextEdit] for a multi-line plain text editor widget. 15010 15011 [TextLabel] for a single line piece of static text. 15012 15013 [TextDisplay] for a read-only display of a larger piece of plain text. 15014 +/ 15015 class LineEdit : EditableTextWidget { 15016 override bool showingVerticalScroll() { return false; } 15017 override bool showingHorizontalScroll() { return false; } 15018 15019 override int flexBasisWidth() { return 250; } 15020 override int widthShrinkiness() { return 10; } 15021 15022 /// 15023 this(Widget parent) { 15024 super(parent); 15025 version(win32_widgets) { 15026 createWin32Window(this, "edit"w, "", 15027 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 15028 } else version(custom_widgets) { 15029 } else static assert(false); 15030 } 15031 15032 private this(bool useCustomWidget, Widget parent) { 15033 if(!useCustomWidget) 15034 this(parent); 15035 else 15036 super(true, parent); 15037 } 15038 15039 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15040 auto tdh = new TextDisplayHelper(textLayout, smw); 15041 tdh.singleLine = true; 15042 return tdh; 15043 } 15044 15045 version(win32_widgets) { 15046 mixin Padding!q{0}; 15047 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 15048 override int maxHeight() { return minHeight; } 15049 } 15050 15051 /+ 15052 @property void passwordMode(bool p) { 15053 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 15054 } 15055 +/ 15056 } 15057 15058 /// ditto 15059 class CustomLineEdit : LineEdit { 15060 this(Widget parent) { 15061 super(true, parent); 15062 } 15063 } 15064 15065 /++ 15066 A [LineEdit] that displays `*` in place of the actual characters. 15067 15068 Alas, Windows requires the window to be created differently to use this style, 15069 so it had to be a new class instead of a toggle on and off on an existing object. 15070 15071 History: 15072 Added January 24, 2021 15073 15074 Implemented on Linux on January 31, 2023. 15075 +/ 15076 class PasswordEdit : EditableTextWidget { 15077 override bool showingVerticalScroll() { return false; } 15078 override bool showingHorizontalScroll() { return false; } 15079 15080 override int flexBasisWidth() { return 250; } 15081 15082 override TextStyle defaultTextStyle() { 15083 auto cs = getComputedStyle(); 15084 15085 auto osf = new class OperatingSystemFont { 15086 this() { 15087 super(cs.font); 15088 } 15089 override fnum stringWidth(scope const(char)[] text, SimpleWindow window = null) { 15090 int count = 0; 15091 foreach(dchar ch; text) 15092 count++; 15093 return count * super.stringWidth("*", window); 15094 } 15095 }; 15096 15097 return new TextDisplayHelper.MyTextStyle(osf); 15098 } 15099 15100 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15101 static class TDH : TextDisplayHelper { 15102 this(TextLayouter textLayout, ScrollMessageWidget smw) { 15103 singleLine = true; 15104 super(textLayout, smw); 15105 } 15106 15107 override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 15108 char[256] buffer = void; 15109 int bufferLength = 0; 15110 foreach(dchar ch; text) 15111 buffer[bufferLength++] = '*'; 15112 painter.setFont(myStyle.font); 15113 painter.drawText(upperLeft, buffer[0..bufferLength]); 15114 } 15115 } 15116 15117 return new TDH(textLayout, smw); 15118 } 15119 15120 /// 15121 this(Widget parent) { 15122 super(parent); 15123 version(win32_widgets) { 15124 createWin32Window(this, "edit"w, "", 15125 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 15126 } else version(custom_widgets) { 15127 } else static assert(false); 15128 } 15129 15130 private this(bool useCustomWidget, Widget parent) { 15131 if(!useCustomWidget) 15132 this(parent); 15133 else 15134 super(true, parent); 15135 } 15136 15137 version(win32_widgets) { 15138 mixin Padding!q{2}; 15139 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 15140 override int maxHeight() { return minHeight; } 15141 } 15142 } 15143 15144 /// ditto 15145 class CustomPasswordEdit : PasswordEdit { 15146 this(Widget parent) { 15147 super(true, parent); 15148 } 15149 } 15150 15151 15152 /++ 15153 A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`. 15154 15155 See_Also: 15156 [TextDisplay] for a read-only text display. 15157 15158 [LineEdit] for a single line text editor. 15159 15160 [PasswordEdit] for a single line text editor that obscures its input. 15161 +/ 15162 class TextEdit : EditableTextWidget { 15163 /// 15164 this(Widget parent) { 15165 super(parent); 15166 version(win32_widgets) { 15167 createWin32Window(this, "edit"w, "", 15168 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 15169 } else version(custom_widgets) { 15170 } else static assert(false); 15171 } 15172 15173 private this(bool useCustomWidget, Widget parent) { 15174 if(!useCustomWidget) 15175 this(parent); 15176 else 15177 super(true, parent); 15178 } 15179 15180 override int maxHeight() { return int.max; } 15181 override int heightStretchiness() { return 7; } 15182 15183 override int flexBasisWidth() { return 250; } 15184 override int flexBasisHeight() { return 25; } 15185 } 15186 15187 /// ditto 15188 class CustomTextEdit : TextEdit { 15189 this(Widget parent) { 15190 super(true, parent); 15191 } 15192 } 15193 15194 /+ 15195 /++ 15196 15197 +/ 15198 version(none) 15199 class RichTextDisplay : Widget { 15200 @property void content(string c) {} 15201 void appendContent(string c) {} 15202 } 15203 +/ 15204 15205 /++ 15206 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. 15207 15208 History: 15209 Added October 31, 2023 (dub v11.3) 15210 +/ 15211 class TextDisplay : EditableTextWidget { 15212 this(string text, Widget parent) { 15213 super(true, parent); 15214 this.content = text; 15215 } 15216 15217 override int maxHeight() { return int.max; } 15218 override int minHeight() { return Window.defaultLineHeight; } 15219 override int heightStretchiness() { return 7; } 15220 override int heightShrinkiness() { return 2; } 15221 15222 override int flexBasisWidth() { 15223 return scaleWithDpi(250); 15224 } 15225 override int flexBasisHeight() { 15226 if(textLayout is null || this.tdh is null) 15227 return Window.defaultLineHeight; 15228 15229 auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height; 15230 return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height; 15231 } 15232 15233 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15234 return new MyTextDisplayHelper(textLayout, smw); 15235 } 15236 15237 override void registerMovement() { 15238 super.registerMovement(); 15239 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 15240 } 15241 15242 static class MyTextDisplayHelper : TextDisplayHelper { 15243 this(TextLayouter textLayout, ScrollMessageWidget smw) { 15244 smw.verticalScrollBar.hide(); 15245 smw.horizontalScrollBar.hide(); 15246 super(textLayout, smw); 15247 this.readonly = true; 15248 } 15249 15250 override void registerMovement() { 15251 super.registerMovement(); 15252 15253 // FIXME: do the horizontal one too as needed and make sure that it does 15254 // wordwrapping again 15255 if(l.height + smw.horizontalScrollBar.height > this.height) 15256 smw.verticalScrollBar.show(); 15257 else 15258 smw.verticalScrollBar.hide(); 15259 15260 l.wordWrapWidth = this.width; 15261 15262 smw.verticalScrollBar.setPosition = 0; 15263 } 15264 } 15265 15266 static class Style : Widget.Style { 15267 // just want the generic look for these 15268 } 15269 15270 mixin OverrideStyle!Style; 15271 } 15272 15273 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 15274 /++ 15275 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. 15276 15277 15278 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 15279 15280 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 15281 15282 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. 15283 15284 History: 15285 Added August 12, 2024 (dub v11.6) 15286 +/ 15287 abstract class GenericListViewWidget : Widget { 15288 /++ 15289 15290 +/ 15291 this(Widget parent) { 15292 super(parent); 15293 15294 smw = new ScrollMessageWidget(this); 15295 smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width); 15296 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 15297 smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet 15298 15299 inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw)); 15300 inner.tabStop = this.tabStop; 15301 this.tabStop = false; 15302 } 15303 15304 private ScrollMessageWidget smw; 15305 private GenericListViewWidgetInner inner; 15306 15307 /++ 15308 15309 +/ 15310 abstract GenericListViewItem itemFactory(Widget parent); 15311 // in device-dependent pixels 15312 /++ 15313 15314 +/ 15315 abstract Size itemSize(); // use 0 to indicate it can stretch? 15316 15317 enum LayoutMode { 15318 rows, 15319 columns, 15320 gridRowsFirst, 15321 gridColumnsFirst 15322 } 15323 LayoutMode layoutMode() { 15324 return LayoutMode.rows; 15325 } 15326 15327 private int itemCount_; 15328 15329 /++ 15330 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. 15331 +/ 15332 void setItemCount(int count) { 15333 smw.setTotalArea(inner.width, count * itemSize().height); 15334 smw.setViewableArea(inner.width, inner.height); 15335 this.itemCount_ = count; 15336 } 15337 15338 /++ 15339 Returns the current count of items expected to available in the list. 15340 +/ 15341 int itemCount() { 15342 return this.itemCount_; 15343 } 15344 15345 /++ 15346 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 15347 15348 Note you must $(I also) call [setItemCount] if the total item count has changed. 15349 +/ 15350 void notifyItemsChanged(int index, int count = 1) { 15351 } 15352 /// ditto 15353 void notifyItemsInserted(int index, int count = 1) { 15354 } 15355 /// ditto 15356 void notifyItemsRemoved(int index, int count = 1) { 15357 } 15358 /// ditto 15359 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 15360 } 15361 15362 /++ 15363 History: 15364 Added January 1, 2025 15365 +/ 15366 void ensureItemVisibleInScroll(int index) { 15367 auto itemPos = index * itemSize().height; 15368 auto vsb = smw.verticalScrollBar; 15369 auto viewable = vsb.viewableArea_; 15370 15371 if(viewable == 0) { 15372 // viewable == 0 isn't actually supposed to happen, this means 15373 // this method is being called before having our size assigned, it should 15374 // probably just queue it up for later. 15375 queuedScroll = index; 15376 return; 15377 } 15378 15379 queuedScroll = int.min; 15380 15381 if(itemPos < vsb.position) { 15382 // scroll up to it 15383 vsb.setPosition(itemPos); 15384 smw.notify(); 15385 } else if(itemPos + itemSize().height > (vsb.position + viewable)) { 15386 // scroll down to it, so it is at the bottom 15387 15388 auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height; 15389 // need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it 15390 15391 vsb.setPosition(itemPos - lastViewableItemPosition); 15392 smw.notify(); 15393 } 15394 } 15395 15396 /++ 15397 History: 15398 Added January 1, 2025; 15399 +/ 15400 int numberOfCurrentlyFullyVisibleItems() { 15401 return smw.verticalScrollBar.viewableArea_ / itemSize.height; 15402 } 15403 15404 private int queuedScroll = int.min; 15405 15406 override void recomputeChildLayout() { 15407 super.recomputeChildLayout(); 15408 if(queuedScroll != int.min) 15409 ensureItemVisibleInScroll(queuedScroll); 15410 } 15411 15412 private GenericListViewItem[] items; 15413 15414 override void paint(WidgetPainter painter) {} 15415 } 15416 15417 /// ditto 15418 abstract class GenericListViewItem : Widget { 15419 /++ 15420 +/ 15421 this(Widget parent) { 15422 super(parent); 15423 } 15424 15425 private int _currentIndex = -1; 15426 15427 private void showItemPrivate(int idx) { 15428 showItem(idx); 15429 _currentIndex = idx; 15430 } 15431 15432 /++ 15433 Implement this to show an item from your data backing to the list. 15434 15435 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. 15436 +/ 15437 abstract void showItem(int idx); 15438 15439 /++ 15440 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 15441 15442 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 15443 15444 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. 15445 +/ 15446 final int currentIndexLoaded() { 15447 return _currentIndex; 15448 } 15449 } 15450 15451 /// 15452 unittest { 15453 import arsd.minigui; 15454 15455 import std.conv; 15456 15457 void main() { 15458 auto mw = new MainWindow(); 15459 15460 static class MyListViewItem : GenericListViewItem { 15461 this(Widget parent) { 15462 super(parent); 15463 15464 label = new TextLabel("unloaded", TextAlignment.Left, this); 15465 button = new Button("Click", this); 15466 15467 button.addEventListener("triggered", (){ 15468 messageBox(text("clicked ", currentIndexLoaded())); 15469 }); 15470 } 15471 override void showItem(int idx) { 15472 label.label = "Item " ~ to!string(idx); 15473 } 15474 15475 TextLabel label; 15476 Button button; 15477 } 15478 15479 auto widget = new class GenericListViewWidget { 15480 this() { 15481 super(mw); 15482 } 15483 override GenericListViewItem itemFactory(Widget parent) { 15484 return new MyListViewItem(parent); 15485 } 15486 override Size itemSize() { 15487 return Size(0, scaleWithDpi(80)); 15488 } 15489 }; 15490 15491 widget.setItemCount(5000); 15492 15493 mw.loop(); 15494 } 15495 } 15496 15497 // this exists just to wrap the actual GenericListViewWidgetInner so borders 15498 // and padding and stuff can work 15499 private class GenericListViewInnerContainer : Widget { 15500 this(Widget parent) { 15501 super(parent); 15502 this.tabStop = false; 15503 } 15504 15505 override void recomputeChildLayout() { 15506 registerMovement(); 15507 15508 auto cs = getComputedStyle(); 15509 auto bw = getBorderWidth(cs.borderStyle); 15510 15511 assert(children.length < 2); 15512 foreach(child; children) { 15513 child.x = bw + paddingLeft(); 15514 child.y = bw + paddingTop(); 15515 child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight(); 15516 child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom(); 15517 15518 child.recomputeChildLayout(); 15519 } 15520 } 15521 15522 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 15523 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15524 return parent.parent.parent.useStyleProperties(dg); 15525 else 15526 return super.useStyleProperties(dg); 15527 } 15528 15529 override int paddingTop() { 15530 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15531 return parent.parent.parent.paddingTop(); 15532 else 15533 return super.paddingTop(); 15534 } 15535 15536 override int paddingBottom() { 15537 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15538 return parent.parent.parent.paddingBottom(); 15539 else 15540 return super.paddingBottom(); 15541 } 15542 15543 override int paddingLeft() { 15544 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15545 return parent.parent.parent.paddingLeft(); 15546 else 15547 return super.paddingLeft(); 15548 } 15549 15550 override int paddingRight() { 15551 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15552 return parent.parent.parent.paddingRight(); 15553 else 15554 return super.paddingRight(); 15555 } 15556 15557 15558 } 15559 15560 private class GenericListViewWidgetInner : Widget { 15561 this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) { 15562 super(parent); 15563 this.glvw = glvw; 15564 15565 reloadVisible(); 15566 15567 smw.addEventListener("scroll", () { 15568 reloadVisible(); 15569 }); 15570 } 15571 15572 override void registerMovement() { 15573 super.registerMovement(); 15574 if(glvw && glvw.smw) 15575 glvw.smw.setViewableArea(this.width, this.height); 15576 } 15577 15578 void reloadVisible() { 15579 auto y = glvw.smw.position.y / glvw.itemSize.height; 15580 15581 // idk why i had this here it doesn't seem to be ueful and actually made last items diasppear 15582 //int offset = glvw.smw.position.y % glvw.itemSize.height; 15583 //if(offset || y >= glvw.itemCount()) 15584 //y--; 15585 15586 if(y < 0) 15587 y = 0; 15588 15589 recomputeChildLayout(); 15590 15591 foreach(item; glvw.items) { 15592 if(y < glvw.itemCount()) { 15593 item.showItemPrivate(y); 15594 item.show(); 15595 } else { 15596 item.hide(); 15597 } 15598 y++; 15599 } 15600 15601 this.redraw(); 15602 } 15603 15604 private GenericListViewWidget glvw; 15605 15606 private bool inRcl; 15607 override void recomputeChildLayout() { 15608 if(inRcl) 15609 return; 15610 inRcl = true; 15611 scope(exit) 15612 inRcl = false; 15613 15614 registerMovement(); 15615 15616 auto ih = glvw.itemSize().height; 15617 15618 auto itemCount = this.height / ih + 2; // extra for partial display before and after 15619 bool hadNew; 15620 while(glvw.items.length < itemCount) { 15621 // FIXME: free the old items? maybe just set length 15622 glvw.items ~= glvw.itemFactory(this); 15623 hadNew = true; 15624 } 15625 15626 if(hadNew) 15627 reloadVisible(); 15628 15629 int y = -(glvw.smw.position.y % ih) + this.paddingTop(); 15630 foreach(child; children) { 15631 child.x = this.paddingLeft(); 15632 child.y = y; 15633 y += glvw.itemSize().height; 15634 child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight(); 15635 child.height = ih; 15636 15637 child.recomputeChildLayout(); 15638 } 15639 } 15640 } 15641 15642 15643 15644 /++ 15645 History: 15646 It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`. 15647 +/ 15648 class MessageBox : Dialog { 15649 private string message; 15650 MessageBoxButton buttonPressed = MessageBoxButton.None; 15651 /++ 15652 15653 History: 15654 The overload that takes `Window originator` was added on September 29, 2024. 15655 +/ 15656 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15657 this(null, message, buttons, buttonIds); 15658 } 15659 /// ditto 15660 this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15661 message = message.stripRightInternal; 15662 int mainWidth; 15663 15664 // estimate longest line 15665 int count; 15666 foreach(ch; message) { 15667 if(ch == '\n') { 15668 if(count > mainWidth) 15669 mainWidth = count; 15670 count = 0; 15671 } else { 15672 count++; 15673 } 15674 } 15675 mainWidth *= 8; 15676 if(mainWidth < 300) 15677 mainWidth = 300; 15678 if(mainWidth > 600) 15679 mainWidth = 600; 15680 15681 super(originator, mainWidth, 100); 15682 15683 assert(buttons.length); 15684 assert(buttons.length == buttonIds.length); 15685 15686 this.message = message; 15687 15688 auto label = new TextDisplay(message, this); 15689 15690 auto hl = new HorizontalLayout(this); 15691 auto spacer = new HorizontalSpacer(hl); // to right align 15692 15693 foreach(idx, buttonText; buttons) { 15694 auto button = new CommandButton(buttonText, hl); 15695 15696 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 15697 this.buttonPressed = buttonIds[idx]; 15698 win.close(); 15699 }; })(idx)); 15700 15701 if(idx == 0) 15702 button.focus(); 15703 } 15704 15705 if(buttons.length == 1) 15706 auto spacer2 = new HorizontalSpacer(hl); // to center it 15707 15708 auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom; 15709 auto max = scaleWithDpi(600); // random max height 15710 if(size > max) 15711 size = max; 15712 15713 win.resize(scaleWithDpi(mainWidth), size); 15714 15715 win.show(); 15716 redraw(); 15717 } 15718 15719 override void OK() { 15720 this.win.close(); 15721 } 15722 15723 mixin Padding!q{16}; 15724 } 15725 15726 /// 15727 enum MessageBoxStyle { 15728 OK, /// 15729 OKCancel, /// 15730 RetryCancel, /// 15731 YesNo, /// 15732 YesNoCancel, /// 15733 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. 15734 } 15735 15736 /// 15737 enum MessageBoxIcon { 15738 None, /// 15739 Info, /// 15740 Warning, /// 15741 Error /// 15742 } 15743 15744 /// Identifies the button the user pressed on a message box. 15745 enum MessageBoxButton { 15746 None, /// The user closed the message box without clicking any of the buttons. 15747 OK, /// 15748 Cancel, /// 15749 Retry, /// 15750 Yes, /// 15751 No, /// 15752 Continue /// 15753 } 15754 15755 15756 /++ 15757 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. 15758 15759 Returns: the button pressed. 15760 +/ 15761 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15762 return messageBox(null, title, message, style, icon); 15763 } 15764 15765 /// ditto 15766 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15767 return messageBox(null, null, message, style, icon); 15768 } 15769 15770 /++ 15771 15772 +/ 15773 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15774 version(win32_widgets) { 15775 WCharzBuffer t = WCharzBuffer(title); 15776 WCharzBuffer m = WCharzBuffer(message); 15777 UINT type; 15778 with(MessageBoxStyle) 15779 final switch(style) { 15780 case OK: type |= MB_OK; break; 15781 case OKCancel: type |= MB_OKCANCEL; break; 15782 case RetryCancel: type |= MB_RETRYCANCEL; break; 15783 case YesNo: type |= MB_YESNO; break; 15784 case YesNoCancel: type |= MB_YESNOCANCEL; break; 15785 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 15786 } 15787 with(MessageBoxIcon) 15788 final switch(icon) { 15789 case None: break; 15790 case Info: type |= MB_ICONINFORMATION; break; 15791 case Warning: type |= MB_ICONWARNING; break; 15792 case Error: type |= MB_ICONERROR; break; 15793 } 15794 switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) { 15795 case IDOK: return MessageBoxButton.OK; 15796 case IDCANCEL: return MessageBoxButton.Cancel; 15797 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 15798 case IDYES: return MessageBoxButton.Yes; 15799 case IDNO: return MessageBoxButton.No; 15800 case IDCONTINUE: return MessageBoxButton.Continue; 15801 default: return MessageBoxButton.None; 15802 } 15803 } else { 15804 string[] buttons; 15805 MessageBoxButton[] buttonIds; 15806 with(MessageBoxStyle) 15807 final switch(style) { 15808 case OK: 15809 buttons = ["OK"]; 15810 buttonIds = [MessageBoxButton.OK]; 15811 break; 15812 case OKCancel: 15813 buttons = ["OK", "Cancel"]; 15814 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 15815 break; 15816 case RetryCancel: 15817 buttons = ["Retry", "Cancel"]; 15818 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 15819 break; 15820 case YesNo: 15821 buttons = ["Yes", "No"]; 15822 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 15823 break; 15824 case YesNoCancel: 15825 buttons = ["Yes", "No", "Cancel"]; 15826 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 15827 break; 15828 case RetryCancelContinue: 15829 buttons = ["Try Again", "Cancel", "Continue"]; 15830 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 15831 break; 15832 } 15833 auto mb = new MessageBox(originator, message, buttons, buttonIds); 15834 EventLoop el = EventLoop.get; 15835 el.run(() { return !mb.win.closed; }); 15836 return mb.buttonPressed; 15837 } 15838 15839 } 15840 15841 /// ditto 15842 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15843 return messageBox(originator, null, message, style, icon); 15844 } 15845 15846 15847 /// 15848 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 15849 15850 /++ 15851 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 15852 15853 History: 15854 The data members were `public` (albeit undocumented and not intended for use) prior to May 13, 2021. They are now `private`, reflecting the single intended use of this object. 15855 +/ 15856 struct EventListener { 15857 private Widget widget; 15858 private string event; 15859 private EventHandler handler; 15860 private bool useCapture; 15861 15862 /// 15863 void disconnect() { 15864 if(widget !is null && handler !is null) 15865 widget.removeEventListener(this); 15866 } 15867 } 15868 15869 /++ 15870 The purpose of this enum was to give a compile-time checked version of various standard event strings. 15871 15872 Now, I recommend you use a statically typed event object instead. 15873 15874 See_Also: [Event] 15875 +/ 15876 enum EventType : string { 15877 click = "click", /// 15878 15879 mouseenter = "mouseenter", /// 15880 mouseleave = "mouseleave", /// 15881 mousein = "mousein", /// 15882 mouseout = "mouseout", /// 15883 mouseup = "mouseup", /// 15884 mousedown = "mousedown", /// 15885 mousemove = "mousemove", /// 15886 15887 keydown = "keydown", /// 15888 keyup = "keyup", /// 15889 char_ = "char", /// 15890 15891 focus = "focus", /// 15892 blur = "blur", /// 15893 15894 triggered = "triggered", /// 15895 15896 change = "change", /// 15897 } 15898 15899 /++ 15900 Represents an event that is currently being processed. 15901 15902 15903 Minigui's event model is based on the web browser. An event has a name, a target, 15904 and an associated data object. It starts from the window and works its way down through 15905 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 15906 then goes back up again all the way back to the window, triggering bubble phase handlers. At 15907 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 15908 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 15909 was called; that just stops it from calling other handlers in the widget tree, but the default happens 15910 whenever propagation is done, not only if it gets to the end of the chain). 15911 15912 This model has several nice points: 15913 15914 $(LIST 15915 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 15916 with event handlers set, then add/remove children as much as you want without needing 15917 to manage the event handlers on them - the parent alone can manage everything. 15918 15919 * It is easy to create new custom events in your application. 15920 15921 * It is familiar to many web developers. 15922 ) 15923 15924 There's a few downsides though: 15925 15926 $(LIST 15927 * There's not a lot of type safety. 15928 15929 * You don't get a static list of what events a widget can emit. 15930 15931 * Tracing where an event got cancelled along the chain can get difficult; the downside of 15932 the central delegation benefit is it can be lead to debugging of action at a distance. 15933 ) 15934 15935 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 15936 while keeping the benefits - and most compatibility with - the existing model. The main idea is 15937 to simply use a D object type which provides a static interface as well as a built-in event name. 15938 Then, a new static interface allows you to see what an event can emit and attach handlers to it 15939 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 15940 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 15941 to having a little more help from the D compiler and documentation generator. 15942 15943 Your code would change like this: 15944 15945 --- 15946 // old 15947 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 15948 15949 // new 15950 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 15951 --- 15952 15953 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. 15954 15955 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 15956 15957 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 15958 15959 Thus the family of functions are: 15960 15961 [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. 15962 15963 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 15964 15965 [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. 15966 15967 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 15968 15969 --- 15970 class MyCheckbox : Widget { 15971 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 15972 /// It is NOT actually required but should be used whenever possible. 15973 mixin Emits!(ChangeEvent!bool); 15974 15975 this(Widget parent) { 15976 super(parent); 15977 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 15978 } 15979 15980 private bool _checked; 15981 @property bool checked() { return _checked; } 15982 @property void checked(bool set) { 15983 _checked = set; 15984 emit!(ChangeEvent!bool)(&checked); 15985 } 15986 } 15987 --- 15988 15989 ## Creating Your Own Events 15990 15991 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. You should mark events `final` unless you specifically plan to use it as a shared base. Only `Widget` and final classes should actually be sent (and preferably, not even `Widget`), with few exceptions. 15992 15993 --- 15994 final class MyEvent : Event { 15995 this(Widget target) { super(EventString, target); } 15996 mixin Register; // adds EventString and other reflection information 15997 } 15998 --- 15999 16000 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 16001 16002 History: 16003 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. 16004 16005 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. 16006 +/ 16007 /+ 16008 16009 ## General Conventions 16010 16011 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. 16012 16013 16014 ## Qt-style signals and slots 16015 16016 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. 16017 16018 The intention is for events to be used when 16019 16020 --- 16021 class Demo : Widget { 16022 this() { 16023 myPropertyChanged = Signal!int(this); 16024 } 16025 @property myProperty(int v) { 16026 myPropertyChanged.emit(v); 16027 } 16028 16029 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 16030 // but it can just genuinely not care about `this` since that's not really passed. 16031 } 16032 16033 class Foo : Widget { 16034 // the slot uda is not necessary, but it helps the script and ui builder find it. 16035 @slot void setValue(int v) { ... } 16036 } 16037 16038 demo.myPropertyChanged.connect(&foo.setValue); 16039 --- 16040 16041 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 16042 16043 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 16044 16045 class StringChangeEvent : ChangeEvent, Signal!string { 16046 mixin SignalImpl 16047 } 16048 16049 +/ 16050 class Event : ReflectableProperties { 16051 /// Creates an event without populating any members and without sending it. See [dispatch] 16052 this(string eventName, Widget emittedBy) { 16053 this.eventName = eventName; 16054 this.srcElement = emittedBy; 16055 } 16056 16057 16058 /// Implementations for the [ReflectableProperties] interface/ 16059 void getPropertiesList(scope void delegate(string name) sink) const {} 16060 /// ditto 16061 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 16062 /// ditto 16063 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 16064 return SetPropertyResult.notPermitted; 16065 } 16066 16067 16068 /+ 16069 /++ 16070 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 16071 16072 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. 16073 +/ 16074 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 16075 if(value.length == 0) { 16076 finalSink(memberName, `""`); 16077 return; 16078 } 16079 16080 char[1024] bufferBacking; 16081 char[] buffer = bufferBacking; 16082 int bufferPosition; 16083 16084 void sink(char ch) { 16085 if(bufferPosition >= buffer.length) 16086 buffer.length = buffer.length + 1024; 16087 buffer[bufferPosition++] = ch; 16088 } 16089 16090 sink('"'); 16091 16092 foreach(ch; value) { 16093 switch(ch) { 16094 case '\\': 16095 sink('\\'); sink('\\'); 16096 break; 16097 case '"': 16098 sink('\\'); sink('"'); 16099 break; 16100 case '\n': 16101 sink('\\'); sink('n'); 16102 break; 16103 case '\r': 16104 sink('\\'); sink('r'); 16105 break; 16106 case '\t': 16107 sink('\\'); sink('t'); 16108 break; 16109 default: 16110 sink(ch); 16111 } 16112 } 16113 16114 sink('"'); 16115 16116 finalSink(memberName, buffer[0 .. bufferPosition]); 16117 } 16118 +/ 16119 16120 /+ 16121 enum EventInitiator { 16122 system, 16123 minigui, 16124 user 16125 } 16126 16127 immutable EventInitiator; initiatedBy; 16128 +/ 16129 16130 /++ 16131 Events should generally follow the propagation model, but there's some exceptions 16132 to that rule. If so, they should override this to return false. In that case, only 16133 bubbling event handlers on the target itself and capturing event handlers on the containing 16134 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 16135 capture -> target -> bubble process.) 16136 16137 History: 16138 Added May 12, 2021 16139 +/ 16140 bool propagates() const pure nothrow @nogc @safe { 16141 return true; 16142 } 16143 16144 /++ 16145 hints as to whether preventDefault will actually do anything. not entirely reliable. 16146 16147 History: 16148 Added May 14, 2021 16149 +/ 16150 bool cancelable() const pure nothrow @nogc @safe { 16151 return true; 16152 } 16153 16154 /++ 16155 You can mix this into child class to register some boilerplate. It includes the `EventString` 16156 member, a constructor, and implementations of the dynamic get data interfaces. 16157 16158 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 16159 16160 16161 You can override the default EventString by simply providing your own in the form of 16162 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 16163 which provides some namespace protection against conflicts in other libraries while still being fairly 16164 easy to use. 16165 16166 If you provide your own constructor, it will override the default constructor provided here. A constructor 16167 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 16168 first argument to your constructor. 16169 16170 History: 16171 Added May 13, 2021. 16172 +/ 16173 protected static mixin template Register() { 16174 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 16175 this(Widget target) { super(EventString, target); } 16176 16177 mixin ReflectableProperties.RegisterGetters; 16178 } 16179 16180 /++ 16181 This is the widget that emitted the event. 16182 16183 16184 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 16185 16186 History: 16187 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 16188 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 16189 so I don't intend to remove these aliases. 16190 +/ 16191 Widget source; 16192 /// ditto 16193 alias source target; 16194 /// ditto 16195 alias source srcElement; 16196 16197 Widget relatedTarget; /// Note: likely to be deprecated at some point. 16198 16199 /// Prevents the default event handler (if there is one) from being called 16200 void preventDefault() { 16201 lastDefaultPrevented = true; 16202 defaultPrevented = true; 16203 } 16204 16205 /// Stops the event propagation immediately. 16206 void stopPropagation() { 16207 propagationStopped = true; 16208 } 16209 16210 private bool defaultPrevented; 16211 private bool propagationStopped; 16212 private string eventName; 16213 16214 private bool isBubbling; 16215 16216 /// 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. 16217 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 16218 16219 /++ 16220 this sends it only to the target. If you want propagation, use dispatch() instead. 16221 16222 This should be made private!!! 16223 16224 +/ 16225 void sendDirectly() { 16226 if(srcElement is null) 16227 return; 16228 16229 // 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. 16230 16231 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 16232 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 16233 16234 if(auto e = target.parentWindow) { 16235 if(auto handlers = "*" in e.capturingEventHandlers) 16236 foreach(handler; *handlers) 16237 if(handler) handler(e, this); 16238 if(auto handlers = eventName in e.capturingEventHandlers) 16239 foreach(handler; *handlers) 16240 if(handler) handler(e, this); 16241 } 16242 16243 auto e = srcElement; 16244 16245 if(auto handlers = eventName in e.bubblingEventHandlers) 16246 foreach(handler; *handlers) 16247 if(handler) handler(e, this); 16248 16249 if(auto handlers = "*" in e.bubblingEventHandlers) 16250 foreach(handler; *handlers) 16251 if(handler) handler(e, this); 16252 16253 // there's never a default for a catch-all event 16254 if(!defaultPrevented) 16255 if(eventName in e.defaultEventHandlers) 16256 e.defaultEventHandlers[eventName](e, this); 16257 } 16258 16259 /// this dispatches the element using the capture -> target -> bubble process 16260 void dispatch() { 16261 if(srcElement is null) 16262 return; 16263 16264 if(!propagates) { 16265 sendDirectly; 16266 return; 16267 } 16268 16269 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 16270 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 16271 16272 // first capture, then bubble 16273 16274 Widget[] chain; 16275 Widget curr = srcElement; 16276 while(curr) { 16277 auto l = curr; 16278 chain ~= l; 16279 curr = curr.parent; 16280 } 16281 16282 isBubbling = false; 16283 16284 foreach_reverse(e; chain) { 16285 if(auto handlers = "*" in e.capturingEventHandlers) 16286 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16287 16288 if(propagationStopped) 16289 break; 16290 16291 if(auto handlers = eventName in e.capturingEventHandlers) 16292 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16293 16294 // the default on capture should really be to always do nothing 16295 16296 //if(!defaultPrevented) 16297 // if(eventName in e.defaultEventHandlers) 16298 // e.defaultEventHandlers[eventName](e.element, this); 16299 16300 if(propagationStopped) 16301 break; 16302 } 16303 16304 int adjustX; 16305 int adjustY; 16306 16307 isBubbling = true; 16308 if(!propagationStopped) 16309 foreach(e; chain) { 16310 if(auto handlers = eventName in e.bubblingEventHandlers) 16311 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16312 16313 if(propagationStopped) 16314 break; 16315 16316 if(auto handlers = "*" in e.bubblingEventHandlers) 16317 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16318 16319 if(propagationStopped) 16320 break; 16321 16322 if(e.encapsulatedChildren()) { 16323 adjustClientCoordinates(adjustX, adjustY); 16324 target = e; 16325 } else { 16326 adjustX += e.x; 16327 adjustY += e.y; 16328 } 16329 } 16330 16331 if(!defaultPrevented) 16332 foreach(e; chain) { 16333 if(eventName in e.defaultEventHandlers) 16334 e.defaultEventHandlers[eventName](e, this); 16335 } 16336 } 16337 16338 16339 /* old compatibility things */ 16340 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 16341 final @property { 16342 Key key() { return (cast(KeyEventBase) this).key; } 16343 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 16344 16345 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 16346 bool altKey() { return (cast(KeyEventBase) this).altKey; } 16347 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 16348 } 16349 16350 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 16351 final @property { 16352 int clientX() { return (cast(MouseEventBase) this).clientX; } 16353 int clientY() { return (cast(MouseEventBase) this).clientY; } 16354 16355 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 16356 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 16357 16358 int button() { return (cast(MouseEventBase) this).button; } 16359 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 16360 } 16361 16362 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 16363 final @property { 16364 int state() { 16365 if(auto meb = cast(MouseEventBase) this) 16366 return meb.state; 16367 if(auto keb = cast(KeyEventBase) this) 16368 return keb.state; 16369 assert(0); 16370 } 16371 } 16372 16373 deprecated("Use a CharEvent instead of Event in your handler going forward") 16374 final @property { 16375 dchar character() { 16376 if(auto ce = cast(CharEvent) this) 16377 return ce.character; 16378 return dchar.init; 16379 } 16380 } 16381 16382 // for change events 16383 @property { 16384 /// 16385 int intValue() { return 0; } 16386 /// 16387 string stringValue() { return null; } 16388 } 16389 } 16390 16391 /++ 16392 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 16393 16394 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 16395 dynamic and custom events, but the static list helps ensure you get them right. 16396 16397 If this is declared, you can use [Widget.emit] to send the event. 16398 16399 All events work the same way though, following the capture->widget->bubble model described under [Event]. 16400 16401 History: 16402 Added May 4, 2021 16403 +/ 16404 mixin template Emits(EventType) { 16405 import arsd.minigui : EventString; 16406 static if(is(EventType : Event) && !is(EventType == Event)) 16407 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 16408 else 16409 static assert(0, "You can only emit subclasses of Event"); 16410 } 16411 16412 /// ditto 16413 mixin template Emits(string eventString) { 16414 mixin("private Event[0] emits_" ~ eventString ~";"); 16415 } 16416 16417 /* 16418 class SignalEvent(string name) : Event { 16419 16420 } 16421 */ 16422 16423 /++ 16424 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". 16425 16426 16427 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. 16428 16429 History: 16430 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 16431 +/ 16432 class CommandEvent : Event { 16433 enum EventString = "command"; 16434 this(Widget source, string CommandString = EventString) { 16435 super(CommandString, source); 16436 } 16437 } 16438 16439 /++ 16440 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 16441 +/ 16442 class CommandEventWithArgs(Args...) : CommandEvent { 16443 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 16444 Args args; 16445 } 16446 16447 /++ 16448 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. 16449 16450 See [CommandEvent] for more information. 16451 16452 Returns: 16453 The [EventListener] you can use to remove the handler. 16454 +/ 16455 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 16456 return w.addEventListener(CommandString, (Event ev) { 16457 if(ev.target is w) 16458 return; // it does not consume its own commands! 16459 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 16460 handler(cev.args); 16461 ev.stopPropagation(); 16462 } 16463 }); 16464 } 16465 16466 /++ 16467 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. 16468 +/ 16469 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 16470 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 16471 event.dispatch(); 16472 } 16473 16474 /++ 16475 Widgets emit `ResizeEvent`s any time they are resized. You check [Widget.width] and [Widget.height] upon receiving this event to know the new size. 16476 16477 If you need to know the old size, you need to store it yourself. 16478 16479 History: 16480 Made final on January 3, 2025 (dub v12.0) 16481 +/ 16482 final class ResizeEvent : Event { 16483 enum EventString = "resize"; 16484 16485 this(Widget target) { super(EventString, target); } 16486 16487 override bool propagates() const { return false; } 16488 } 16489 16490 /++ 16491 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 16492 16493 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. 16494 16495 History: 16496 Added June 21, 2021 (dub v10.1) 16497 16498 Made final on January 3, 2025 (dub v12.0) 16499 +/ 16500 final class ClosingEvent : Event { 16501 enum EventString = "closing"; 16502 16503 this(Widget target) { super(EventString, target); } 16504 16505 override bool propagates() const { return false; } 16506 override bool cancelable() const { return true; } 16507 } 16508 16509 /// ditto 16510 final class ClosedEvent : Event { 16511 enum EventString = "closed"; 16512 16513 this(Widget target) { super(EventString, target); } 16514 16515 override bool propagates() const { return false; } 16516 override bool cancelable() const { return false; } 16517 } 16518 16519 /// 16520 final class BlurEvent : Event { 16521 enum EventString = "blur"; 16522 16523 // FIXME: related target? 16524 this(Widget target) { super(EventString, target); } 16525 16526 override bool propagates() const { return false; } 16527 } 16528 16529 /// 16530 final class FocusEvent : Event { 16531 enum EventString = "focus"; 16532 16533 // FIXME: related target? 16534 this(Widget target) { super(EventString, target); } 16535 16536 override bool propagates() const { return false; } 16537 } 16538 16539 /++ 16540 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 16541 16542 History: 16543 Added July 3, 2021 16544 +/ 16545 final class FocusInEvent : Event { 16546 enum EventString = "focusin"; 16547 16548 // FIXME: related target? 16549 this(Widget target) { super(EventString, target); } 16550 16551 override bool cancelable() const { return false; } 16552 } 16553 16554 /// ditto 16555 final class FocusOutEvent : Event { 16556 enum EventString = "focusout"; 16557 16558 // FIXME: related target? 16559 this(Widget target) { super(EventString, target); } 16560 16561 override bool cancelable() const { return false; } 16562 } 16563 16564 /// 16565 final class ScrollEvent : Event { 16566 enum EventString = "scroll"; 16567 this(Widget target) { super(EventString, target); } 16568 16569 override bool cancelable() const { return false; } 16570 } 16571 16572 /++ 16573 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 16574 16575 History: 16576 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 16577 +/ 16578 final class CharEvent : Event { 16579 enum EventString = "char"; 16580 this(Widget target, dchar ch) { 16581 character = ch; 16582 super(EventString, target); 16583 } 16584 16585 immutable dchar character; 16586 } 16587 16588 /++ 16589 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 16590 +/ 16591 abstract class ChangeEventBase : Event { 16592 enum EventString = "change"; 16593 this(Widget target) { 16594 super(EventString, target); 16595 } 16596 16597 /+ 16598 // idk where or how exactly i want to do this. 16599 // i might come back to it later. 16600 16601 // If a widget itself broadcasts one of theses itself, it stops propagation going down 16602 // this way the source doesn't get too confused (think of a nested scroll widget) 16603 // 16604 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 16605 // then you consume that command and change you scroll x position to whatever. then you do 16606 // some kind of change event that is broadcast back to the children and any horizontal scroll 16607 // listeners are now able to update, without having an explicit connection between them. 16608 void broadcastToChildren(string fieldName) { 16609 16610 } 16611 +/ 16612 } 16613 16614 /++ 16615 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. 16616 16617 16618 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). 16619 16620 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);` 16621 16622 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 16623 16624 History: 16625 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. 16626 +/ 16627 final class ChangeEvent(T) : ChangeEventBase { 16628 this(Widget target, T delegate() getNewValue) { 16629 assert(getNewValue !is null); 16630 this.getNewValue = getNewValue; 16631 super(target); 16632 } 16633 16634 private T delegate() getNewValue; 16635 16636 /++ 16637 Gets the new value that just changed. 16638 +/ 16639 @property T value() { 16640 return getNewValue(); 16641 } 16642 16643 /// compatibility method for old generic Events 16644 static if(is(immutable T == immutable int)) 16645 override int intValue() { return value; } 16646 /// ditto 16647 static if(is(immutable T == immutable string)) 16648 override string stringValue() { return value; } 16649 } 16650 16651 /++ 16652 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 16653 16654 16655 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16656 16657 History: 16658 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16659 +/ 16660 abstract class KeyEventBase : Event { 16661 this(string name, Widget target) { 16662 super(name, target); 16663 } 16664 16665 // for key events 16666 Key key; /// 16667 16668 KeyEvent originalKeyEvent; 16669 16670 /++ 16671 Indicates the current state of the given keyboard modifier keys. 16672 16673 History: 16674 Added to events on April 15, 2020. 16675 +/ 16676 bool ctrlKey; 16677 16678 /// ditto 16679 bool altKey; 16680 16681 /// ditto 16682 bool shiftKey; 16683 16684 /++ 16685 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 16686 16687 See [arsd.simpledisplay.ModifierState] for other possible flags. 16688 +/ 16689 int state; 16690 16691 mixin Register; 16692 } 16693 16694 /++ 16695 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]. 16696 16697 16698 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16699 16700 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. 16701 16702 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. 16703 16704 See_Also: [KeyUpEvent], [CharEvent] 16705 16706 History: 16707 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 16708 +/ 16709 final class KeyDownEvent : KeyEventBase { 16710 enum EventString = "keydown"; 16711 this(Widget target) { super(EventString, target); } 16712 } 16713 16714 /++ 16715 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 16716 16717 16718 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16719 16720 See_Also: [KeyDownEvent], [CharEvent] 16721 16722 History: 16723 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 16724 +/ 16725 final class KeyUpEvent : KeyEventBase { 16726 enum EventString = "keyup"; 16727 this(Widget target) { super(EventString, target); } 16728 } 16729 16730 /++ 16731 Contains shared properties for various mouse events; 16732 16733 16734 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16735 16736 History: 16737 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16738 +/ 16739 abstract class MouseEventBase : Event { 16740 this(string name, Widget target) { 16741 super(name, target); 16742 } 16743 16744 // for mouse events 16745 int clientX; /// The mouse event location relative to the target widget 16746 int clientY; /// ditto 16747 16748 int viewportX; /// The mouse event location relative to the window origin 16749 int viewportY; /// ditto 16750 16751 int button; /// See: [MouseEvent.button] 16752 int buttonLinear; /// See: [MouseEvent.buttonLinear] 16753 16754 /++ 16755 Indicates the current state of the given keyboard modifier keys. 16756 16757 History: 16758 Added to mouse events on September 28, 2010. 16759 +/ 16760 bool ctrlKey; 16761 16762 /// ditto 16763 bool altKey; 16764 16765 /// ditto 16766 bool shiftKey; 16767 16768 16769 16770 int state; /// 16771 16772 /++ 16773 for consistent names with key event. 16774 16775 History: 16776 Added September 28, 2021 (dub v10.3) 16777 +/ 16778 alias modifierState = state; 16779 16780 /++ 16781 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 16782 16783 History: 16784 Added May 15, 2021 16785 +/ 16786 bool isMouseWheel() { 16787 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 16788 } 16789 16790 // private 16791 override void adjustClientCoordinates(int deltaX, int deltaY) { 16792 clientX += deltaX; 16793 clientY += deltaY; 16794 } 16795 16796 mixin Register; 16797 } 16798 16799 /++ 16800 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 16801 16802 16803 $(WARNING 16804 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 16805 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 16806 behavior. 16807 16808 Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others. 16809 ) 16810 16811 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 16812 16813 [MouseUpEvent] is sent when the user releases a mouse button. 16814 16815 [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.) 16816 16817 [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. 16818 16819 [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 different 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. 16820 16821 [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. 16822 16823 [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. 16824 16825 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 16826 16827 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 16828 16829 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16830 16831 Rationale: 16832 16833 If you only want to do drag, mousedown/up works just fine being consistently sent. 16834 16835 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). 16836 16837 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. 16838 16839 History: 16840 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. 16841 +/ 16842 final class MouseUpEvent : MouseEventBase { 16843 enum EventString = "mouseup"; /// 16844 this(Widget target) { super(EventString, target); } 16845 } 16846 /// ditto 16847 final class MouseDownEvent : MouseEventBase { 16848 enum EventString = "mousedown"; /// 16849 this(Widget target) { super(EventString, target); } 16850 } 16851 /// ditto 16852 final class MouseMoveEvent : MouseEventBase { 16853 enum EventString = "mousemove"; /// 16854 this(Widget target) { super(EventString, target); } 16855 } 16856 /// ditto 16857 final class ClickEvent : MouseEventBase { 16858 enum EventString = "click"; /// 16859 this(Widget target) { super(EventString, target); } 16860 } 16861 /// ditto 16862 final class DoubleClickEvent : MouseEventBase { 16863 enum EventString = "dblclick"; /// 16864 this(Widget target) { super(EventString, target); } 16865 } 16866 /// ditto 16867 final class MouseOverEvent : Event { 16868 enum EventString = "mouseover"; /// 16869 this(Widget target) { super(EventString, target); } 16870 } 16871 /// ditto 16872 final class MouseOutEvent : Event { 16873 enum EventString = "mouseout"; /// 16874 this(Widget target) { super(EventString, target); } 16875 } 16876 /// ditto 16877 final class MouseEnterEvent : Event { 16878 enum EventString = "mouseenter"; /// 16879 this(Widget target) { super(EventString, target); } 16880 16881 override bool propagates() const { return false; } 16882 } 16883 /// ditto 16884 final class MouseLeaveEvent : Event { 16885 enum EventString = "mouseleave"; /// 16886 this(Widget target) { super(EventString, target); } 16887 16888 override bool propagates() const { return false; } 16889 } 16890 16891 private bool isAParentOf(Widget a, Widget b) { 16892 if(a is null || b is null) 16893 return false; 16894 16895 while(b !is null) { 16896 if(a is b) 16897 return true; 16898 b = b.parent; 16899 } 16900 16901 return false; 16902 } 16903 16904 private struct WidgetAtPointResponse { 16905 Widget widget; 16906 16907 // x, y relative to the widget in the response. 16908 int x; 16909 int y; 16910 } 16911 16912 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 16913 assert(starting !is null); 16914 16915 starting.addScrollPosition(x, y); 16916 16917 auto child = starting.getChildAtPosition(x, y); 16918 while(child) { 16919 if(child.hidden) 16920 continue; 16921 starting = child; 16922 x -= child.x; 16923 y -= child.y; 16924 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 16925 child = r.widget; 16926 if(child is starting) 16927 break; 16928 } 16929 return WidgetAtPointResponse(starting, x, y); 16930 } 16931 16932 version(win32_widgets) { 16933 private: 16934 import core.sys.windows.commctrl; 16935 16936 pragma(lib, "comctl32"); 16937 shared static this() { 16938 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 16939 INITCOMMONCONTROLSEX ic; 16940 ic.dwSize = cast(DWORD) ic.sizeof; 16941 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 16942 if(!InitCommonControlsEx(&ic)) { 16943 //writeln("ICC failed"); 16944 } 16945 } 16946 16947 16948 // everything from here is just win32 headers copy pasta 16949 private: 16950 extern(Windows): 16951 16952 alias HANDLE HMENU; 16953 HMENU CreateMenu(); 16954 bool SetMenu(HWND, HMENU); 16955 HMENU CreatePopupMenu(); 16956 enum MF_POPUP = 0x10; 16957 enum MF_STRING = 0; 16958 16959 16960 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 16961 struct INITCOMMONCONTROLSEX { 16962 DWORD dwSize; 16963 DWORD dwICC; 16964 } 16965 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 16966 enum { 16967 IDB_STD_SMALL_COLOR, 16968 IDB_STD_LARGE_COLOR, 16969 IDB_VIEW_SMALL_COLOR = 4, 16970 IDB_VIEW_LARGE_COLOR = 5 16971 } 16972 enum { 16973 STD_CUT, 16974 STD_COPY, 16975 STD_PASTE, 16976 STD_UNDO, 16977 STD_REDOW, 16978 STD_DELETE, 16979 STD_FILENEW, 16980 STD_FILEOPEN, 16981 STD_FILESAVE, 16982 STD_PRINTPRE, 16983 STD_PROPERTIES, 16984 STD_HELP, 16985 STD_FIND, 16986 STD_REPLACE, 16987 STD_PRINT // = 14 16988 } 16989 16990 alias HANDLE HIMAGELIST; 16991 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 16992 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 16993 BOOL ImageList_Destroy(HIMAGELIST); 16994 16995 uint MAKELONG(ushort a, ushort b) { 16996 return cast(uint) ((b << 16) | a); 16997 } 16998 16999 17000 struct TBBUTTON { 17001 int iBitmap; 17002 int idCommand; 17003 BYTE fsState; 17004 BYTE fsStyle; 17005 version(Win64) 17006 BYTE[6] bReserved; 17007 else 17008 BYTE[2] bReserved; 17009 DWORD dwData; 17010 INT_PTR iString; 17011 } 17012 17013 enum { 17014 TB_ADDBUTTONSA = WM_USER + 20, 17015 TB_INSERTBUTTONA = WM_USER + 21, 17016 TB_GETIDEALSIZE = WM_USER + 99, 17017 } 17018 17019 struct SIZE { 17020 LONG cx; 17021 LONG cy; 17022 } 17023 17024 17025 enum { 17026 TBSTATE_CHECKED = 1, 17027 TBSTATE_PRESSED = 2, 17028 TBSTATE_ENABLED = 4, 17029 TBSTATE_HIDDEN = 8, 17030 TBSTATE_INDETERMINATE = 16, 17031 TBSTATE_WRAP = 32 17032 } 17033 17034 17035 17036 enum { 17037 ILC_COLOR = 0, 17038 ILC_COLOR4 = 4, 17039 ILC_COLOR8 = 8, 17040 ILC_COLOR16 = 16, 17041 ILC_COLOR24 = 24, 17042 ILC_COLOR32 = 32, 17043 ILC_COLORDDB = 254, 17044 ILC_MASK = 1, 17045 ILC_PALETTE = 2048 17046 } 17047 17048 17049 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 17050 17051 17052 enum { 17053 TB_ENABLEBUTTON = WM_USER + 1, 17054 TB_CHECKBUTTON, 17055 TB_PRESSBUTTON, 17056 TB_HIDEBUTTON, 17057 TB_INDETERMINATE, // = WM_USER + 5, 17058 TB_ISBUTTONENABLED = WM_USER + 9, 17059 TB_ISBUTTONCHECKED, 17060 TB_ISBUTTONPRESSED, 17061 TB_ISBUTTONHIDDEN, 17062 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 17063 TB_SETSTATE = WM_USER + 17, 17064 TB_GETSTATE = WM_USER + 18, 17065 TB_ADDBITMAP = WM_USER + 19, 17066 TB_DELETEBUTTON = WM_USER + 22, 17067 TB_GETBUTTON, 17068 TB_BUTTONCOUNT, 17069 TB_COMMANDTOINDEX, 17070 TB_SAVERESTOREA, 17071 TB_CUSTOMIZE, 17072 TB_ADDSTRINGA, 17073 TB_GETITEMRECT, 17074 TB_BUTTONSTRUCTSIZE, 17075 TB_SETBUTTONSIZE, 17076 TB_SETBITMAPSIZE, 17077 TB_AUTOSIZE, // = WM_USER + 33, 17078 TB_GETTOOLTIPS = WM_USER + 35, 17079 TB_SETTOOLTIPS = WM_USER + 36, 17080 TB_SETPARENT = WM_USER + 37, 17081 TB_SETROWS = WM_USER + 39, 17082 TB_GETROWS, 17083 TB_GETBITMAPFLAGS, 17084 TB_SETCMDID, 17085 TB_CHANGEBITMAP, 17086 TB_GETBITMAP, 17087 TB_GETBUTTONTEXTA, 17088 TB_REPLACEBITMAP, // = WM_USER + 46, 17089 TB_GETBUTTONSIZE = WM_USER + 58, 17090 TB_SETBUTTONWIDTH = WM_USER + 59, 17091 TB_GETBUTTONTEXTW = WM_USER + 75, 17092 TB_SAVERESTOREW = WM_USER + 76, 17093 TB_ADDSTRINGW = WM_USER + 77, 17094 } 17095 17096 extern(Windows) 17097 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 17098 17099 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 17100 17101 17102 enum { 17103 TB_SETINDENT = WM_USER + 47, 17104 TB_SETIMAGELIST, 17105 TB_GETIMAGELIST, 17106 TB_LOADIMAGES, 17107 TB_GETRECT, 17108 TB_SETHOTIMAGELIST, 17109 TB_GETHOTIMAGELIST, 17110 TB_SETDISABLEDIMAGELIST, 17111 TB_GETDISABLEDIMAGELIST, 17112 TB_SETSTYLE, 17113 TB_GETSTYLE, 17114 //TB_GETBUTTONSIZE, 17115 //TB_SETBUTTONWIDTH, 17116 TB_SETMAXTEXTROWS, 17117 TB_GETTEXTROWS // = WM_USER + 61 17118 } 17119 17120 enum { 17121 CCM_FIRST = 0x2000, 17122 CCM_LAST = CCM_FIRST + 0x200, 17123 CCM_SETBKCOLOR = 8193, 17124 CCM_SETCOLORSCHEME = 8194, 17125 CCM_GETCOLORSCHEME = 8195, 17126 CCM_GETDROPTARGET = 8196, 17127 CCM_SETUNICODEFORMAT = 8197, 17128 CCM_GETUNICODEFORMAT = 8198, 17129 CCM_SETVERSION = 0x2007, 17130 CCM_GETVERSION = 0x2008, 17131 CCM_SETNOTIFYWINDOW = 0x2009 17132 } 17133 17134 17135 enum { 17136 PBM_SETRANGE = WM_USER + 1, 17137 PBM_SETPOS, 17138 PBM_DELTAPOS, 17139 PBM_SETSTEP, 17140 PBM_STEPIT, // = WM_USER + 5 17141 PBM_SETRANGE32 = 1030, 17142 PBM_GETRANGE, 17143 PBM_GETPOS, 17144 PBM_SETBARCOLOR, // = 1033 17145 PBM_SETBKCOLOR = CCM_SETBKCOLOR 17146 } 17147 17148 enum { 17149 PBS_SMOOTH = 1, 17150 PBS_VERTICAL = 4 17151 } 17152 17153 enum { 17154 ICC_LISTVIEW_CLASSES = 1, 17155 ICC_TREEVIEW_CLASSES = 2, 17156 ICC_BAR_CLASSES = 4, 17157 ICC_TAB_CLASSES = 8, 17158 ICC_UPDOWN_CLASS = 16, 17159 ICC_PROGRESS_CLASS = 32, 17160 ICC_HOTKEY_CLASS = 64, 17161 ICC_ANIMATE_CLASS = 128, 17162 ICC_WIN95_CLASSES = 255, 17163 ICC_DATE_CLASSES = 256, 17164 ICC_USEREX_CLASSES = 512, 17165 ICC_COOL_CLASSES = 1024, 17166 ICC_STANDARD_CLASSES = 0x00004000, 17167 } 17168 17169 enum WM_USER = 1024; 17170 } 17171 17172 version(win32_widgets) 17173 pragma(lib, "comdlg32"); 17174 17175 17176 /// 17177 enum GenericIcons : ushort { 17178 None, /// 17179 // these happen to match the win32 std icons numerically if you just subtract one from the value 17180 Cut, /// 17181 Copy, /// 17182 Paste, /// 17183 Undo, /// 17184 Redo, /// 17185 Delete, /// 17186 New, /// 17187 Open, /// 17188 Save, /// 17189 PrintPreview, /// 17190 Properties, /// 17191 Help, /// 17192 Find, /// 17193 Replace, /// 17194 Print, /// 17195 } 17196 17197 enum FileDialogType { 17198 Automatic, 17199 Open, 17200 Save 17201 } 17202 17203 /++ 17204 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. 17205 +/ 17206 string previousFileReferenced; 17207 17208 /++ 17209 Used in automatic menu functions to indicate that the user should be able to browse for a file. 17210 17211 Params: 17212 storage = an alias to a `static string` variable that stores the last file referenced. It will 17213 use this to pre-fill the dialog with a suggestion. 17214 17215 Please note that it MUST be `static` or you will get compile errors. 17216 17217 filters = the filters param to [getFileName] 17218 17219 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 17220 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 17221 a save dialog box. Otherwise, it will show an open dialog box. 17222 +/ 17223 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 17224 string name; 17225 alias name this; 17226 17227 @implicit this(string name) { 17228 this.name = name; 17229 } 17230 } 17231 17232 /++ 17233 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. 17234 17235 History: 17236 onCancel was added November 6, 2021. 17237 17238 The dialog itself on Linux was modified on December 2, 2021 to include 17239 a directory picker in addition to the command line completion view. 17240 17241 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 17242 17243 The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version. 17244 Future_directions: 17245 I want to add some kind of custom preview and maybe thumbnail thing in the future, 17246 at least on Linux, maybe on Windows too. 17247 +/ 17248 void getOpenFileName( 17249 Window owner, 17250 void delegate(string) onOK, 17251 string prefilledName = null, 17252 string[] filters = null, 17253 void delegate() onCancel = null, 17254 string initialDirectory = null, 17255 ) 17256 { 17257 return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory); 17258 } 17259 17260 /// ditto 17261 void getSaveFileName( 17262 Window owner, 17263 void delegate(string) onOK, 17264 string prefilledName = null, 17265 string[] filters = null, 17266 void delegate() onCancel = null, 17267 string initialDirectory = null, 17268 ) 17269 { 17270 return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory); 17271 } 17272 17273 // 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.") 17274 /// ditto 17275 void getOpenFileName( 17276 void delegate(string) onOK, 17277 string prefilledName = null, 17278 string[] filters = null, 17279 void delegate() onCancel = null, 17280 string initialDirectory = null, 17281 ) 17282 { 17283 return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory); 17284 } 17285 17286 /// ditto 17287 void getSaveFileName( 17288 void delegate(string) onOK, 17289 string prefilledName = null, 17290 string[] filters = null, 17291 void delegate() onCancel = null, 17292 string initialDirectory = null, 17293 ) 17294 { 17295 return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory); 17296 } 17297 17298 /++ 17299 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. 17300 17301 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. 17302 17303 History: 17304 Added January 1, 2025 17305 +/ 17306 class FileDialogDelegate { 17307 17308 /++ 17309 17310 +/ 17311 static abstract class PreviewWidget : Widget { 17312 /// Call this from your subclass' constructor 17313 this(Widget parent) { 17314 super(parent); 17315 } 17316 17317 /// Load the file given to you and show its preview inside the widget here 17318 abstract void previewFile(string filename); 17319 } 17320 17321 /++ 17322 Override this to add preview capabilities to the dialog for certain files. 17323 +/ 17324 protected PreviewWidget makePreviewWidget(Widget parent) { 17325 return null; 17326 } 17327 17328 /++ 17329 Override this to change the dialog entirely. 17330 17331 This function IS allowed to block, but is NOT required to. 17332 +/ 17333 protected void getFileName( 17334 Window owner, 17335 bool openOrSave, // true if open, false if save 17336 void delegate(string) onOK, 17337 string prefilledName, 17338 string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17339 void delegate() onCancel, 17340 string initialDirectory, 17341 ) 17342 { 17343 17344 version(win32_widgets) { 17345 import core.sys.windows.commdlg; 17346 /* 17347 Ofn.lStructSize = sizeof(OPENFILENAME); 17348 Ofn.hwndOwner = hWnd; 17349 Ofn.lpstrFilter = szFilter; 17350 Ofn.lpstrFile= szFile; 17351 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 17352 Ofn.lpstrFileTitle = szFileTitle; 17353 Ofn.nMaxFileTitle = sizeof(szFileTitle); 17354 Ofn.lpstrInitialDir = (LPSTR)NULL; 17355 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 17356 Ofn.lpstrTitle = szTitle; 17357 */ 17358 17359 17360 wchar[1024] file = 0; 17361 wchar[1024] filterBuffer = 0; 17362 makeWindowsString(prefilledName, file[]); 17363 OPENFILENAME ofn; 17364 ofn.lStructSize = ofn.sizeof; 17365 ofn.hwndOwner = owner is null ? null : owner.win.hwnd; 17366 if(filters.length) { 17367 string filter; 17368 foreach(i, f; filters) { 17369 filter ~= f; 17370 filter ~= "\0"; 17371 } 17372 filter ~= "\0"; 17373 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 17374 } 17375 ofn.lpstrFile = file.ptr; 17376 ofn.nMaxFile = file.length; 17377 17378 wchar[1024] initialDir = 0; 17379 if(initialDirectory !is null) { 17380 makeWindowsString(initialDirectory, initialDir[]); 17381 ofn.lpstrInitialDir = file.ptr; 17382 } 17383 17384 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 17385 { 17386 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 17387 if(okString.length && okString[$-1] == '\0') 17388 okString = okString[0..$-1]; 17389 onOK(okString); 17390 } else { 17391 if(onCancel) 17392 onCancel(); 17393 } 17394 } else version(custom_widgets) { 17395 filters ~= ["All Files\0*.*"]; 17396 auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner); 17397 picker.onOK = onOK; 17398 picker.onCancel = onCancel; 17399 picker.show(); 17400 } 17401 } 17402 17403 } 17404 17405 /// ditto 17406 FileDialogDelegate fileDialogDelegate() { 17407 if(fileDialogDelegate_ is null) 17408 fileDialogDelegate_ = new FileDialogDelegate(); 17409 return fileDialogDelegate_; 17410 } 17411 17412 /// ditto 17413 void fileDialogDelegate(FileDialogDelegate replacement) { 17414 fileDialogDelegate_ = replacement; 17415 } 17416 17417 private FileDialogDelegate fileDialogDelegate_; 17418 17419 struct FileNameFilter { 17420 string description; 17421 string[] globPatterns; 17422 17423 string toString() { 17424 string ret; 17425 ret ~= description; 17426 ret ~= " ("; 17427 foreach(idx, pattern; globPatterns) { 17428 if(idx) 17429 ret ~= "; "; 17430 ret ~= pattern; 17431 } 17432 ret ~= ")"; 17433 17434 return ret; 17435 } 17436 17437 static FileNameFilter fromString(string s) { 17438 size_t end = s.length; 17439 size_t start = 0; 17440 foreach_reverse(idx, ch; s) { 17441 if(ch == ')' && end == s.length) 17442 end = idx; 17443 else if(ch == '(' && end != s.length) { 17444 start = idx + 1; 17445 break; 17446 } 17447 } 17448 17449 FileNameFilter fnf; 17450 fnf.description = s[0 .. start ? start - 1 : 0]; 17451 size_t globStart = 0; 17452 s = s[start .. end]; 17453 foreach(idx, ch; s) 17454 if(ch == ';') { 17455 auto ptn = stripInternal(s[globStart .. idx]); 17456 if(ptn.length) 17457 fnf.globPatterns ~= ptn; 17458 globStart = idx + 1; 17459 17460 } 17461 auto ptn = stripInternal(s[globStart .. $]); 17462 if(ptn.length) 17463 fnf.globPatterns ~= ptn; 17464 return fnf; 17465 } 17466 } 17467 17468 struct FileNameFilterSet { 17469 FileNameFilter[] filters; 17470 17471 static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) { 17472 FileNameFilter[] ret; 17473 17474 foreach(filter; filters) { 17475 FileNameFilter fnf; 17476 size_t filterStartPoint; 17477 foreach(idx, ch; filter) { 17478 if(ch == 0) { 17479 fnf.description = filter[0 .. idx]; 17480 filterStartPoint = idx + 1; 17481 } else if(filterStartPoint && ch == ';') { 17482 fnf.globPatterns ~= filter[filterStartPoint .. idx]; 17483 filterStartPoint = idx + 1; 17484 } 17485 } 17486 fnf.globPatterns ~= filter[filterStartPoint .. $]; 17487 17488 ret ~= fnf; 17489 } 17490 17491 return FileNameFilterSet(ret); 17492 } 17493 } 17494 17495 void getFileName( 17496 Window owner, 17497 bool openOrSave, 17498 void delegate(string) onOK, 17499 string prefilledName = null, 17500 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17501 void delegate() onCancel = null, 17502 string initialDirectory = null, 17503 ) 17504 { 17505 return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory); 17506 } 17507 17508 version(custom_widgets) 17509 private 17510 class FilePicker : Dialog { 17511 void delegate(string) onOK; 17512 void delegate() onCancel; 17513 LabeledLineEdit lineEdit; 17514 bool isOpenDialogInsteadOfSave; 17515 17516 static struct HistoryItem { 17517 string cwd; 17518 FileNameFilter filters; 17519 } 17520 HistoryItem[] historyStack; 17521 size_t historyStackPosition; 17522 17523 void back() { 17524 if(historyStackPosition) { 17525 historyStackPosition--; 17526 currentDirectory = historyStack[historyStackPosition].cwd; 17527 currentFilter = historyStack[historyStackPosition].filters; 17528 filesOfType.content = currentFilter.toString(); 17529 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 17530 lineEdit.focus(); 17531 } 17532 } 17533 17534 void forward() { 17535 if(historyStackPosition + 1 < historyStack.length) { 17536 historyStackPosition++; 17537 currentDirectory = historyStack[historyStackPosition].cwd; 17538 currentFilter = historyStack[historyStackPosition].filters; 17539 filesOfType.content = currentFilter.toString(); 17540 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 17541 lineEdit.focus(); 17542 } 17543 } 17544 17545 void up() { 17546 currentDirectory = currentDirectory ~ ".."; 17547 loadFiles(currentDirectory, currentFilter); 17548 lineEdit.focus(); 17549 } 17550 17551 void refresh() { 17552 loadFiles(currentDirectory, currentFilter); 17553 lineEdit.focus(); 17554 } 17555 17556 // returns common prefix 17557 static struct CommonPrefixInfo { 17558 string commonPrefix; 17559 int fileCount; 17560 string exactMatch; 17561 } 17562 CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) { 17563 17564 if(!comingFromHistory) { 17565 if(historyStack.length) { 17566 historyStack = historyStack[0 .. historyStackPosition + 1]; 17567 historyStack.assumeSafeAppend(); 17568 } 17569 historyStack ~= HistoryItem(cwd, filters); 17570 historyStackPosition = historyStack.length - 1; 17571 } 17572 17573 string[] files; 17574 string[] dirs; 17575 17576 dirs ~= "$HOME"; 17577 dirs ~= "$PWD"; 17578 17579 string commonPrefix; 17580 int commonPrefixCount; 17581 string exactMatch; 17582 17583 bool matchesFilter(string name) { 17584 foreach(filter; filters.globPatterns) { 17585 if( 17586 filter.length <= 1 || 17587 filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .* 17588 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 17589 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 17590 ) 17591 { 17592 if(name.length > 1 && name[0] == '.') 17593 if(filter.length == 0 || filter[0] != '.') 17594 return false; 17595 17596 return true; 17597 } 17598 } 17599 17600 return false; 17601 } 17602 17603 void considerCommonPrefix(string name, bool prefiltered) { 17604 if(!prefiltered && !matchesFilter(name)) 17605 return; 17606 17607 if(commonPrefix is null) { 17608 commonPrefix = name; 17609 commonPrefixCount = 1; 17610 exactMatch = commonPrefix; 17611 } else { 17612 foreach(idx, char i; name) { 17613 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 17614 commonPrefix = commonPrefix[0 .. idx]; 17615 commonPrefixCount ++; 17616 exactMatch = null; 17617 break; 17618 } 17619 } 17620 } 17621 } 17622 17623 bool applyFilterToDirectories = true; 17624 bool showDotFiles = false; 17625 foreach(filter; filters.globPatterns) { 17626 if(filter == ".*") 17627 showDotFiles = true; 17628 else foreach(ch; filter) 17629 if(ch == '.') { 17630 // a filter like *.exe should not apply to the directory 17631 applyFilterToDirectories = false; 17632 break; 17633 } 17634 } 17635 17636 try 17637 getFiles(cwd, (string name, bool isDirectory) { 17638 if(name == ".") 17639 return; // skip this as unnecessary 17640 if(isDirectory) { 17641 if(applyFilterToDirectories) { 17642 if(matchesFilter(name)) { 17643 dirs ~= name; 17644 considerCommonPrefix(name, false); 17645 } 17646 } else if(name != ".." && name.length > 1 && name[0] == '.') { 17647 if(showDotFiles) { 17648 dirs ~= name; 17649 considerCommonPrefix(name, false); 17650 } 17651 } else { 17652 dirs ~= name; 17653 considerCommonPrefix(name, false); 17654 } 17655 } else { 17656 if(matchesFilter(name)) { 17657 files ~= name; 17658 17659 //if(filter.length > 0 && filter[$-1] == '*') { 17660 considerCommonPrefix(name, true); 17661 //} 17662 } 17663 } 17664 }); 17665 catch(ArsdExceptionBase e) { 17666 messageBox("Unable to read requested directory"); 17667 // FIXME: give them a chance to create it? or at least go back? 17668 /+ 17669 comingFromHistory = true; 17670 back(); 17671 return null; 17672 +/ 17673 } 17674 17675 extern(C) static int comparator(scope const void* a, scope const void* b) { 17676 auto sa = *cast(string*) a; 17677 auto sb = *cast(string*) b; 17678 17679 /+ 17680 Goal here: 17681 17682 Dot first. This puts `foo.d` before `foo2.d` 17683 Then numbers , natural sort order (so 9 comes before 10) for positive numbers 17684 Then letters, in order Aa, Bb, Cc 17685 Then other symbols in ascii order 17686 +/ 17687 static int nextPiece(ref string whole) { 17688 if(whole.length == 0) 17689 return -1; 17690 17691 enum specialZoneSize = 1; 17692 17693 char current = whole[0]; 17694 if(current >= '0' && current <= '9') { 17695 int accumulator; 17696 do { 17697 whole = whole[1 .. $]; 17698 accumulator *= 10; 17699 accumulator += current - '0'; 17700 current = whole.length ? whole[0] : 0; 17701 } while (current >= '0' && current <= '9'); 17702 17703 return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols 17704 } else { 17705 whole = whole[1 .. $]; 17706 17707 if(current == '.') 17708 return 0; // the special case to put it before numbers 17709 17710 // anything above should be < specialZoneSize 17711 17712 int letterZoneSize = 26 * 2; 17713 int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars 17714 17715 if(current >= 'A' && current <= 'Z') 17716 return base + (current - 'A') * 2; 17717 if(current >= 'a' && current <= 'z') 17718 return base + (current - 'a') * 2 + 1; 17719 // return base + letterZoneSize + current; // would put symbols after numbers and letters 17720 return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone 17721 } 17722 } 17723 17724 while(sa.length || sb.length) { 17725 auto pa = nextPiece(sa); 17726 auto pb = nextPiece(sb); 17727 17728 auto diff = pa - pb; 17729 if(diff) 17730 return diff; 17731 } 17732 17733 return 0; 17734 } 17735 17736 nonPhobosSort(files, &comparator); 17737 nonPhobosSort(dirs, &comparator); 17738 17739 listWidget.clear(); 17740 dirWidget.clear(); 17741 foreach(name; dirs) 17742 dirWidget.addOption(name); 17743 foreach(name; files) 17744 listWidget.addOption(name); 17745 17746 return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch); 17747 } 17748 17749 ListWidget listWidget; 17750 ListWidget dirWidget; 17751 17752 FreeEntrySelection filesOfType; 17753 LineEdit directoryHolder; 17754 17755 string currentDirectory_; 17756 FileNameFilter currentNonTabFilter; 17757 FileNameFilter currentFilter; 17758 FileNameFilterSet filterOptions; 17759 17760 void currentDirectory(string s) { 17761 currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString(); 17762 directoryHolder.content = currentDirectory_; 17763 } 17764 string currentDirectory() { 17765 return currentDirectory_; 17766 } 17767 17768 private string getUserHomeDir() { 17769 import core.stdc.stdlib; 17770 version(Windows) 17771 return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup; 17772 else 17773 return (stringz(getenv("HOME")).borrow).idup; 17774 } 17775 17776 private string expandTilde(string s) { 17777 // FIXME: cannot look up other user dirs 17778 if(s.length == 1 && s == "~") 17779 return getUserHomeDir(); 17780 if(s.length > 1 && s[0] == '~' && s[1] == '/') 17781 return getUserHomeDir() ~ s[1 .. $]; 17782 return s; 17783 } 17784 17785 // FIXME: allow many files to be picked too sometimes 17786 17787 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17788 this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) { 17789 this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat); 17790 this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave; 17791 super(owner, 500, 400, "Choose File..."); // owner); 17792 17793 { 17794 auto navbar = new HorizontalLayout(24, this); 17795 auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar); 17796 auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar); 17797 auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button 17798 17799 directoryHolder = new LineEdit(navbar); 17800 17801 directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) { 17802 if(kde.key == Key.Enter || kde.key == Key.PadEnter) { 17803 kde.stopPropagation(); 17804 17805 currentDirectory = directoryHolder.content; 17806 loadFiles(currentDirectory, currentFilter); 17807 17808 lineEdit.focus(); 17809 } 17810 }); 17811 17812 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. 17813 17814 /+ 17815 auto newDirectoryButton = new ToolButton(new Action("N"), navbar); 17816 17817 // FIXME: make sure putting `.` in the dir filter goes back to the CWD 17818 // and that ~ goes back to the home dir 17819 // and blanking it goes back to the suggested dir 17820 17821 auto homeButton = new ToolButton(new Action("H"), navbar); 17822 auto cwdButton = new ToolButton(new Action("."), navbar); 17823 auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar); 17824 +/ 17825 17826 filesOfType = new class FreeEntrySelection { 17827 this() { 17828 string[] opt; 17829 foreach(option; filterOptions.filters) 17830 opt ~= option.toString; 17831 super(opt, navbar); 17832 } 17833 override int flexBasisWidth() { 17834 return scaleWithDpi(150); 17835 } 17836 override int widthStretchiness() { 17837 return 1;//super.widthStretchiness() / 2; 17838 } 17839 }; 17840 filesOfType.setSelection(0); 17841 currentFilter = filterOptions.filters[0]; 17842 currentNonTabFilter = currentFilter; 17843 } 17844 17845 { 17846 auto mainGrid = new GridLayout(4, 1, this); 17847 17848 dirWidget = new ListWidget(mainGrid); 17849 listWidget = new ListWidget(mainGrid); 17850 listWidget.tabStop = false; 17851 dirWidget.tabStop = false; 17852 17853 FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid); 17854 17855 mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1); 17856 mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1); 17857 if(previewWidget) 17858 mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1); 17859 17860 // double click events normally trigger something else but 17861 // here user might be clicking kinda fast and we'd rather just 17862 // keep it 17863 dirWidget.addEventListener((scope DoubleClickEvent dev) { 17864 auto ce = new ChangeEvent!void(dirWidget, () {}); 17865 ce.dispatch(); 17866 lineEdit.focus(); 17867 }); 17868 17869 dirWidget.addEventListener((scope ChangeEvent!void sce) { 17870 string v; 17871 foreach(o; dirWidget.options) 17872 if(o.selected) { 17873 v = o.label; 17874 break; 17875 } 17876 if(v.length) { 17877 if(v == "$HOME") 17878 currentDirectory = getUserHomeDir(); 17879 else if(v == "$PWD") 17880 currentDirectory = "."; 17881 else 17882 currentDirectory = currentDirectory ~ "/" ~ v; 17883 loadFiles(currentDirectory, currentFilter); 17884 } 17885 17886 dirWidget.focusOn = -1; 17887 lineEdit.focus(); 17888 }); 17889 17890 // double click here, on the other hand, selects the file 17891 // and moves on 17892 listWidget.addEventListener((scope DoubleClickEvent dev) { 17893 OK(); 17894 }); 17895 } 17896 17897 lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this); 17898 lineEdit.focus(); 17899 lineEdit.addEventListener(delegate(CharEvent event) { 17900 if(event.character == '\t' || event.character == '\n') 17901 event.preventDefault(); 17902 }); 17903 17904 listWidget.addEventListener(EventType.change, () { 17905 foreach(o; listWidget.options) 17906 if(o.selected) 17907 lineEdit.content = o.label; 17908 }); 17909 17910 currentDirectory = initialDirectory is null ? "." : initialDirectory; 17911 17912 auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory)); 17913 currentDirectory = prefilledPath.directoryName; 17914 prefilledName = prefilledPath.filename; 17915 loadFiles(currentDirectory, currentFilter); 17916 17917 filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) { 17918 currentFilter = FileNameFilter.fromString(ce.stringValue); 17919 currentNonTabFilter = currentFilter; 17920 loadFiles(currentDirectory, currentFilter); 17921 // lineEdit.focus(); // this causes a recursive crash..... 17922 }); 17923 17924 filesOfType.addEventListener(delegate(KeyDownEvent event) { 17925 if(event.key == Key.Enter) { 17926 currentFilter = FileNameFilter.fromString(filesOfType.content); 17927 currentNonTabFilter = currentFilter; 17928 loadFiles(currentDirectory, currentFilter); 17929 event.stopPropagation(); 17930 // FIXME: refocus on the line edit 17931 } 17932 }); 17933 17934 lineEdit.addEventListener((KeyDownEvent event) { 17935 if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) { 17936 17937 auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory)); 17938 currentDirectory = path.directoryName; 17939 auto current = path.filename; 17940 17941 auto newFilter = current; 17942 if(current.length && current[0] != '*' && current[$-1] != '*') 17943 newFilter ~= "*"; 17944 else if(newFilter.length == 0) 17945 newFilter = "*"; 17946 17947 auto newFilterObj = FileNameFilter("Custom filter", [newFilter]); 17948 17949 CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj); 17950 if(commonPrefix.fileCount == 1) { 17951 // exactly one file, let's see what it is 17952 auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory)); 17953 if(getFileType(specificFile.toString) == FileType.dir) { 17954 // a directory means we should change to it and keep the old filter 17955 currentDirectory = specificFile.toString(); 17956 lineEdit.content = specificFile.toString() ~ "/"; 17957 loadFiles(currentDirectory, currentFilter); 17958 } else { 17959 // any other file should be selected in the list 17960 currentDirectory = specificFile.directoryName; 17961 current = specificFile.filename; 17962 lineEdit.content = current; 17963 loadFiles(currentDirectory, currentFilter); 17964 } 17965 } else if(commonPrefix.fileCount > 1) { 17966 currentFilter = newFilterObj; 17967 filesOfType.content = currentFilter.toString(); 17968 lineEdit.content = commonPrefix.commonPrefix; 17969 } else { 17970 // if there were no files, we don't really want to change the filter.. 17971 //sdpyPrintDebugString("no files"); 17972 } 17973 17974 // FIXME: if that is a directory, add the slash? or even go inside? 17975 17976 event.preventDefault(); 17977 } 17978 else if(event.key == Key.Left && event.altKey) { 17979 this.back(); 17980 event.preventDefault(); 17981 } 17982 else if(event.key == Key.Right && event.altKey) { 17983 this.forward(); 17984 event.preventDefault(); 17985 } 17986 }); 17987 17988 17989 lineEdit.content = prefilledName; 17990 17991 auto hl = new HorizontalLayout(60, this); 17992 auto cancelButton = new Button("Cancel", hl); 17993 auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl); 17994 17995 cancelButton.addEventListener(EventType.triggered, &Cancel); 17996 okButton.addEventListener(EventType.triggered, &OK); 17997 17998 this.addEventListener((KeyDownEvent event) { 17999 if(event.key == Key.Enter || event.key == Key.PadEnter) { 18000 event.preventDefault(); 18001 OK(); 18002 } 18003 else if(event.key == Key.Escape) 18004 Cancel(); 18005 else if(event.key == Key.F5) 18006 refresh(); 18007 else if(event.key == Key.Up && event.altKey) 18008 up(); // ditto 18009 else if(event.key == Key.Left && event.altKey) 18010 back(); // FIXME: it sends the key to the line edit too 18011 else if(event.key == Key.Right && event.altKey) 18012 forward(); // ditto 18013 else if(event.key == Key.Up) 18014 listWidget.setSelection(listWidget.getSelection() - 1); 18015 else if(event.key == Key.Down) 18016 listWidget.setSelection(listWidget.getSelection() + 1); 18017 }); 18018 18019 // FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted 18020 // FIXME: button to create new directory 18021 // FIXME: show dirs in the files list too? idk. 18022 18023 // FIXME: support ~ as alias for home in the input 18024 // FIXME: tab complete ought to be able to change+complete dir too 18025 } 18026 18027 override void OK() { 18028 if(lineEdit.content.length) { 18029 auto c = expandTilde(lineEdit.content); 18030 18031 FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory)); 18032 18033 auto ft = getFileType(accepted.toString); 18034 18035 if(ft == FileType.error && isOpenDialogInsteadOfSave) { 18036 // FIXME: tell the user why 18037 messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel."); 18038 lineEdit.focus(); 18039 return; 18040 18041 } 18042 18043 // FIXME: symlinks to dirs should prolly also get this behavior 18044 if(ft == FileType.dir) { 18045 currentDirectory = accepted.toString; 18046 18047 currentFilter = currentNonTabFilter; 18048 filesOfType.content = currentFilter.toString(); 18049 18050 loadFiles(currentDirectory, currentFilter); 18051 lineEdit.content = ""; 18052 18053 lineEdit.focus(); 18054 18055 return; 18056 } 18057 18058 if(onOK) 18059 onOK(accepted.toString); 18060 } 18061 close(); 18062 } 18063 18064 override void Cancel() { 18065 if(onCancel) 18066 onCancel(); 18067 close(); 18068 } 18069 } 18070 18071 private enum FileType { 18072 error, 18073 dir, 18074 other 18075 } 18076 18077 private FileType getFileType(string name) { 18078 version(Windows) { 18079 auto ws = WCharzBuffer(name); 18080 auto ret = GetFileAttributesW(ws.ptr); 18081 if(ret == INVALID_FILE_ATTRIBUTES) 18082 return FileType.error; 18083 return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other; 18084 } else version(Posix) { 18085 import core.sys.posix.sys.stat; 18086 stat_t buf; 18087 auto ret = stat((name ~ '\0').ptr, &buf); 18088 if(ret == -1) 18089 return FileType.error; 18090 return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other; 18091 } else assert(0, "Not implemented"); 18092 } 18093 18094 /* 18095 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 18096 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 18097 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 18098 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 18099 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 18100 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 18101 http://www.sbin.org/doc/Xlib/chapt_03.html 18102 18103 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 18104 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 18105 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 18106 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 18107 */ 18108 18109 18110 // These are all for setMenuAndToolbarFromAnnotatedCode 18111 /// This item in the menu will be preceded by a separator line 18112 /// Group: generating_from_code 18113 struct separator {} 18114 deprecated("It was misspelled, use separator instead") alias seperator = separator; 18115 /// Program-wide keyboard shortcut to trigger the action 18116 /// Group: generating_from_code 18117 struct accelerator { string keyString; } // FIXME: allow multiple aliases here 18118 /// tells which menu the action will be on 18119 /// Group: generating_from_code 18120 struct menu { string name; } 18121 /// Describes which toolbar section the action appears on 18122 /// Group: generating_from_code 18123 struct toolbar { string groupName; } 18124 /// 18125 /// Group: generating_from_code 18126 struct icon { ushort id; } 18127 /// 18128 /// Group: generating_from_code 18129 struct label { string label; } 18130 /// 18131 /// Group: generating_from_code 18132 struct hotkey { dchar ch; } 18133 /// 18134 /// Group: generating_from_code 18135 struct tip { string tip; } 18136 /// 18137 /// Group: generating_from_code 18138 enum context_menu = menu.init; 18139 /++ 18140 // FIXME: the options should have both a label and a value 18141 18142 if label is null, it will try to just stringify value. 18143 18144 if type is int or size_t and it returns a string array, we can use the index but this will implicitly not allow custom, even if allowCustom is set. 18145 +/ 18146 /// Group: generating_from_code 18147 Choices!T choices(T)(T[] options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) { 18148 return Choices!T(() => options, allowCustom, allowReordering, allowDuplicates); 18149 } 18150 /// ditto 18151 Choices!T choices(T)(T[] delegate() options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) { 18152 return Choices!T(options, allowCustom, allowReordering, allowDuplicates); 18153 } 18154 /// ditto 18155 struct Choices(T) { 18156 /// 18157 T[] delegate() options; 18158 bool allowCustom = false; 18159 /// only relevant if attached to an array 18160 bool allowReordering = true; 18161 /// ditto 18162 bool allowDuplicates = true; 18163 /// makes no sense on a set 18164 bool requireAll = false; 18165 } 18166 18167 18168 /++ 18169 Observes and allows inspection of an object via automatic gui 18170 +/ 18171 /// Group: generating_from_code 18172 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 18173 return new ObjectInspectionWindowImpl!(T)(t); 18174 } 18175 18176 class ObjectInspectionWindow : Window { 18177 this(int a, int b, string c) { 18178 super(a, b, c); 18179 } 18180 18181 abstract void readUpdatesFromObject(); 18182 } 18183 18184 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 18185 T t; 18186 this(T t) { 18187 this.t = t; 18188 18189 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 18190 18191 foreach(memberName; __traits(derivedMembers, T)) {{ 18192 alias member = I!(__traits(getMember, t, memberName))[0]; 18193 alias type = typeof(member); 18194 static if(is(type == int)) { 18195 auto le = new LabeledLineEdit(memberName ~ ": ", this); 18196 //le.addEventListener("char", (Event ev) { 18197 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 18198 //ev.preventDefault(); 18199 //}); 18200 le.addEventListener(EventType.change, (Event ev) { 18201 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 18202 }); 18203 18204 updateMemberDelegates[memberName] = () { 18205 le.content = toInternal!string(__traits(getMember, t, memberName)); 18206 }; 18207 } 18208 }} 18209 } 18210 18211 void delegate()[string] updateMemberDelegates; 18212 18213 override void readUpdatesFromObject() { 18214 foreach(k, v; updateMemberDelegates) 18215 v(); 18216 } 18217 } 18218 18219 /++ 18220 Creates a dialog based on a data structure. 18221 18222 --- 18223 dialog(window, (YourStructure value) { 18224 // the user filled in the struct and clicked OK, 18225 // you can check the members now 18226 }); 18227 --- 18228 18229 Params: 18230 initialData = the initial value to show in the dialog. It will not modify this unless 18231 it is a class then it might, no promises. 18232 18233 History: 18234 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 18235 18236 The overloads with `parent` were added September 29, 2024. The ones without it are likely to 18237 be deprecated soon. 18238 +/ 18239 /// Group: generating_from_code 18240 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18241 dialog(null, T.init, onOK, onCancel, title); 18242 } 18243 /// ditto 18244 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18245 dialog(null, T.init, onOK, onCancel, title); 18246 } 18247 /// ditto 18248 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18249 dialog(parent, T.init, onOK, onCancel, title); 18250 } 18251 /// ditto 18252 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18253 dialog(parent, initialData, onOK, onCancel, title); 18254 } 18255 /// ditto 18256 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18257 auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title); 18258 dg.show(); 18259 } 18260 18261 private static template I(T...) { alias I = T; } 18262 18263 18264 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 18265 if(name == "id") 18266 return allLowerCase ? name : "ID"; 18267 18268 char[160] buffer; 18269 int bufferIndex = 0; 18270 bool shouldCap = true; 18271 bool shouldSpace; 18272 bool lastWasCap; 18273 foreach(idx, char ch; name) { 18274 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 18275 18276 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 18277 if(lastWasCap) { 18278 // two caps in a row, don't change. Prolly acronym. 18279 } else { 18280 if(idx) 18281 shouldSpace = true; // new word, add space 18282 } 18283 18284 lastWasCap = true; 18285 } else { 18286 lastWasCap = false; 18287 } 18288 18289 if(shouldSpace) { 18290 buffer[bufferIndex++] = space; 18291 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 18292 shouldSpace = false; 18293 } 18294 if(shouldCap) { 18295 if(ch >= 'a' && ch <= 'z') 18296 ch -= 32; 18297 shouldCap = false; 18298 } 18299 if(allLowerCase && ch >= 'A' && ch <= 'Z') 18300 ch += 32; 18301 buffer[bufferIndex++] = ch; 18302 } 18303 return buffer[0 .. bufferIndex].idup; 18304 } 18305 18306 /++ 18307 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. 18308 +/ 18309 class AutomaticDialog(T) : Dialog { 18310 T t; 18311 18312 void delegate(T) onOK; 18313 void delegate() onCancel; 18314 18315 override int paddingTop() { return defaultLineHeight; } 18316 override int paddingBottom() { return defaultLineHeight; } 18317 override int paddingRight() { return defaultLineHeight; } 18318 override int paddingLeft() { return defaultLineHeight; } 18319 18320 this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 18321 assert(onOK !is null); 18322 18323 t = initialData; 18324 18325 static if(is(T == class)) { 18326 if(t is null) 18327 t = new T(); 18328 } 18329 this.onOK = onOK; 18330 this.onCancel = onCancel; 18331 super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 18332 18333 static if(is(T == class)) 18334 this.addDataControllerWidget(t); 18335 else 18336 this.addDataControllerWidget(&t); 18337 18338 auto hl = new HorizontalLayout(this); 18339 auto stretch = new HorizontalSpacer(hl); // to right align 18340 auto ok = new CommandButton("OK", hl); 18341 auto cancel = new CommandButton("Cancel", hl); 18342 ok.addEventListener(EventType.triggered, &OK); 18343 cancel.addEventListener(EventType.triggered, &Cancel); 18344 18345 this.addEventListener((KeyDownEvent ev) { 18346 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 18347 ok.focus(); 18348 OK(); 18349 ev.preventDefault(); 18350 } 18351 if(ev.key == Key.Escape) { 18352 Cancel(); 18353 ev.preventDefault(); 18354 } 18355 }); 18356 18357 this.addEventListener((scope ClosedEvent ce) { 18358 if(onCancel) 18359 onCancel(); 18360 }); 18361 18362 //this.children[0].focus(); 18363 } 18364 18365 override void OK() { 18366 onOK(t); 18367 close(); 18368 } 18369 18370 override void Cancel() { 18371 if(onCancel) 18372 onCancel(); 18373 close(); 18374 } 18375 } 18376 18377 private template baseClassCount(Class) { 18378 private int helper() { 18379 int count = 0; 18380 static if(is(Class bases == super)) { 18381 foreach(base; bases) 18382 static if(is(base == class)) 18383 count += 1 + baseClassCount!base; 18384 } 18385 return count; 18386 } 18387 18388 enum int baseClassCount = helper(); 18389 } 18390 18391 private long stringToLong(string s) { 18392 long ret; 18393 if(s.length == 0) 18394 return ret; 18395 bool negative = s[0] == '-'; 18396 if(negative) 18397 s = s[1 .. $]; 18398 foreach(ch; s) { 18399 if(ch >= '0' && ch <= '9') { 18400 ret *= 10; 18401 ret += ch - '0'; 18402 } 18403 } 18404 if(negative) 18405 ret = -ret; 18406 return ret; 18407 } 18408 18409 18410 interface ReflectableProperties { 18411 /++ 18412 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 18413 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 18414 json in the current implementation. 18415 18416 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 18417 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 18418 as of the June 2, 2021 release. 18419 18420 History: 18421 Added June 2, 2021. 18422 18423 See_Also: [getPropertyAsString], [setPropertyFromString] 18424 +/ 18425 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 18426 /++ 18427 Requests a property to be delivered to you as a string, through your `sink` delegate. 18428 18429 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 18430 be interpreted as json, otherwise, it is just a plain string. 18431 18432 The sink should always be called exactly once for each call (it is basically a return value, but it might 18433 use a local buffer it maintains instead of allocating a return value). 18434 18435 History: 18436 Added June 2, 2021. 18437 18438 See_Also: [getPropertiesList], [setPropertyFromString] 18439 +/ 18440 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 18441 /++ 18442 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. 18443 18444 History: 18445 Added June 2, 2021. 18446 18447 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 18448 +/ 18449 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 18450 18451 /// [setPropertyFromString] possible return values 18452 enum SetPropertyResult { 18453 success = 0, /// the property has been successfully set to the request value 18454 notPermitted = -1, /// the property exists but it cannot be changed at this time 18455 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 18456 noSuchProperty = -3, /// there is no property by that name 18457 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 18458 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) 18459 } 18460 18461 /++ 18462 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 18463 18464 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 18465 18466 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 18467 rarely need to use these building blocks directly. 18468 +/ 18469 mixin template RegisterSetters() { 18470 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 18471 switch(name) { 18472 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18473 case memberName: 18474 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 18475 if(value != "true" && value != "false") 18476 return SetPropertyResult.wrongFormat; 18477 __traits(getMember, this, memberName) = value == "true" ? true : false; 18478 return SetPropertyResult.success; 18479 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 18480 import core.stdc.stdlib; 18481 char[128] zero = 0; 18482 if(buffer.length + 1 >= zero.length) 18483 return SetPropertyResult.wrongFormat; 18484 zero[0 .. buffer.length] = buffer[]; 18485 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 18486 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 18487 import core.stdc.stdlib; 18488 char[128] zero = 0; 18489 if(buffer.length + 1 >= zero.length) 18490 return SetPropertyResult.wrongFormat; 18491 zero[0 .. buffer.length] = buffer[]; 18492 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 18493 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 18494 __traits(getMember, this, memberName) = value.idup; 18495 } else { 18496 return SetPropertyResult.notImplemented; 18497 } 18498 18499 } 18500 default: 18501 return super.setPropertyFromString(name, value, valueIsJson); 18502 } 18503 } 18504 } 18505 18506 /++ 18507 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 18508 18509 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 18510 18511 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 18512 rarely need to use these building blocks directly. 18513 +/ 18514 mixin template RegisterGetters() { 18515 override void getPropertiesList(scope void delegate(string name) sink) const { 18516 super.getPropertiesList(sink); 18517 18518 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18519 sink(memberName); 18520 } 18521 } 18522 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 18523 switch(name) { 18524 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18525 case memberName: 18526 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 18527 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 18528 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 18529 import core.stdc.stdio; 18530 char[32] buffer; 18531 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 18532 sink(name, buffer[0 .. len], true); 18533 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 18534 import core.stdc.stdio; 18535 char[32] buffer; 18536 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 18537 sink(name, buffer[0 .. len], true); 18538 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 18539 sink(name, __traits(getMember, this, memberName), false); 18540 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 18541 } else { 18542 sink(name, null, true); 18543 } 18544 18545 return; 18546 } 18547 default: 18548 return super.getPropertyAsString(name, sink); 18549 } 18550 } 18551 } 18552 } 18553 18554 private struct Stack(T) { 18555 this(int maxSize) { 18556 internalLength = 0; 18557 arr = initialBuffer[]; 18558 } 18559 18560 ///. 18561 void push(T t) { 18562 if(internalLength >= arr.length) { 18563 auto oldarr = arr; 18564 if(arr.length < 4096) 18565 arr = new T[arr.length * 2]; 18566 else 18567 arr = new T[arr.length + 4096]; 18568 arr[0 .. oldarr.length] = oldarr[]; 18569 } 18570 18571 arr[internalLength] = t; 18572 internalLength++; 18573 } 18574 18575 ///. 18576 T pop() { 18577 assert(internalLength); 18578 internalLength--; 18579 return arr[internalLength]; 18580 } 18581 18582 ///. 18583 T peek() { 18584 assert(internalLength); 18585 return arr[internalLength - 1]; 18586 } 18587 18588 ///. 18589 @property bool empty() { 18590 return internalLength ? false : true; 18591 } 18592 18593 ///. 18594 private T[] arr; 18595 private size_t internalLength; 18596 private T[64] initialBuffer; 18597 // 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), 18598 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 18599 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 18600 } 18601 18602 /// 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. 18603 private struct WidgetStream { 18604 18605 ///. 18606 @property Widget front() { 18607 return current.widget; 18608 } 18609 18610 /// Use Widget.tree instead. 18611 this(Widget start) { 18612 current.widget = start; 18613 current.childPosition = -1; 18614 isEmpty = false; 18615 stack = typeof(stack)(0); 18616 } 18617 18618 /* 18619 Handle it 18620 handle its children 18621 18622 */ 18623 18624 ///. 18625 void popFront() { 18626 more: 18627 if(isEmpty) return; 18628 18629 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 18630 18631 current.childPosition++; 18632 if(current.childPosition >= current.widget.children.length) { 18633 if(stack.empty()) 18634 isEmpty = true; 18635 else { 18636 current = stack.pop(); 18637 goto more; 18638 } 18639 } else { 18640 stack.push(current); 18641 current.widget = current.widget.children[current.childPosition]; 18642 current.childPosition = -1; 18643 } 18644 } 18645 18646 ///. 18647 @property bool empty() { 18648 return isEmpty; 18649 } 18650 18651 private: 18652 18653 struct Current { 18654 Widget widget; 18655 int childPosition; 18656 } 18657 18658 Current current; 18659 18660 Stack!(Current) stack; 18661 18662 bool isEmpty; 18663 } 18664 18665 18666 /+ 18667 18668 I could fix up the hierarchy kinda like this 18669 18670 class Widget { 18671 Widget[] children() { return null; } 18672 } 18673 interface WidgetContainer { 18674 Widget asWidget(); 18675 void addChild(Widget w); 18676 18677 // alias asWidget this; // but meh 18678 } 18679 18680 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 18681 18682 class Layout : Widget, WidgetContainer {} 18683 18684 class Window : WidgetContainer {} 18685 18686 18687 All constructors that previously took Widgets should now take WidgetContainers instead 18688 18689 18690 18691 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". 18692 +/ 18693 18694 /+ 18695 LAYOUTS 2.0 18696 18697 can just be assigned as a function. assigning a new one will cause it to be immediately called. 18698 18699 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 18700 18701 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 18702 18703 and even Paint can just use computedStyle... 18704 18705 background color 18706 font 18707 border color and style 18708 18709 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 18710 please note that many widgets and in some modes will completely ignore properties as they will. 18711 they are just hints you set, not promises. 18712 18713 18714 18715 18716 18717 So generally the existing virtual functions are just the default for the class. But individual objects 18718 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 18719 +/ 18720 18721 /++ 18722 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. 18723 18724 History: 18725 Added May 24, 2021. 18726 +/ 18727 struct WidgetBackground { 18728 /++ 18729 A background with the given solid color. 18730 +/ 18731 this(Color color) { 18732 this.color = color; 18733 } 18734 18735 this(WidgetBackground bg) { 18736 this = bg; 18737 } 18738 18739 /++ 18740 Creates a widget from the string. 18741 18742 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 18743 +/ 18744 static WidgetBackground fromString(string s) { 18745 return WidgetBackground(Color.fromString(s)); 18746 } 18747 18748 /++ 18749 The background is not necessarily a solid color, but you can always specify a color as a fallback. 18750 18751 History: 18752 Made `public` on December 18, 2022 (dub v10.10). 18753 +/ 18754 Color color; 18755 } 18756 18757 /++ 18758 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!) 18759 18760 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. 18761 18762 You should not inherit from this directly, but instead use [VisualTheme]. 18763 18764 History: 18765 Added May 8, 2021 18766 +/ 18767 abstract class BaseVisualTheme { 18768 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 18769 abstract void doPaint(Widget widget, WidgetPainter painter); 18770 18771 /+ 18772 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 18773 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 18774 +/ 18775 18776 /++ 18777 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 18778 where the interpretation of the string varies for each property and may include things like measurement units. 18779 +/ 18780 abstract string getPropertyString(Widget widget, string propertyName); 18781 18782 /++ 18783 Default background color of the window. Widgets also use this to simulate transparency. 18784 18785 Probably some shade of grey. 18786 +/ 18787 abstract Color windowBackgroundColor(); 18788 abstract Color widgetBackgroundColor(); 18789 abstract Color foregroundColor(); 18790 abstract Color lightAccentColor(); 18791 abstract Color darkAccentColor(); 18792 18793 /++ 18794 Colors used to indicate active selections in lists and text boxes, etc. 18795 +/ 18796 abstract Color selectionForegroundColor(); 18797 /// ditto 18798 abstract Color selectionBackgroundColor(); 18799 18800 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 18801 18802 /++ 18803 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 18804 +/ 18805 abstract OperatingSystemFont defaultFont(int dpi); 18806 18807 private OperatingSystemFont[int] defaultFontCache_; 18808 private OperatingSystemFont defaultFontCached(int dpi) { 18809 if(dpi !in defaultFontCache_) { 18810 // FIXME: set this to false if X disconnect or if visual theme changes 18811 defaultFontCache_[dpi] = defaultFont(dpi); 18812 } 18813 return defaultFontCache_[dpi]; 18814 } 18815 } 18816 18817 /+ 18818 A widget should have: 18819 classList 18820 dataset 18821 attributes 18822 computedStyles 18823 state (persistent) 18824 dynamic state (focused, hover, etc) 18825 +/ 18826 18827 // visualTheme.computedStyle(this).paddingLeft 18828 18829 18830 /++ 18831 This is your entry point to create your own visual theme for custom widgets. 18832 18833 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 18834 18835 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 18836 +/ 18837 abstract class VisualTheme(CRTP) : BaseVisualTheme { 18838 override string getPropertyString(Widget widget, string propertyName) { 18839 return null; 18840 } 18841 18842 /+ 18843 mixin StyleOverride!Widget 18844 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 18845 w.useStyleProperties(dg); 18846 } 18847 +/ 18848 18849 final override void doPaint(Widget widget, WidgetPainter painter) { 18850 auto derived = cast(CRTP) cast(void*) this; 18851 18852 scope void delegate(Widget, WidgetPainter) bestMatch; 18853 int bestMatchScore; 18854 18855 static if(__traits(hasMember, CRTP, "paint")) 18856 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 18857 static if(is(typeof(overload) Params == __parameters)) { 18858 static assert(Params.length == 2); 18859 static assert(is(Params[0] : Widget)); 18860 static assert(is(Params[1] == WidgetPainter)); 18861 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); 18862 18863 alias type = Params[0]; 18864 if(cast(type) widget) { 18865 auto score = baseClassCount!type; 18866 18867 if(score > bestMatchScore) { 18868 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 18869 bestMatchScore = score; 18870 } 18871 } 18872 } else static assert(0, "paint should be a method."); 18873 } 18874 18875 if(bestMatch) 18876 bestMatch(widget, painter); 18877 else 18878 widget.paint(painter); 18879 } 18880 18881 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 18882 18883 // 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 18884 // mixin Beautiful95Theme; 18885 mixin DefaultLightTheme; 18886 18887 private static struct Cached { 18888 // i prolly want to do this 18889 } 18890 } 18891 18892 /// ditto 18893 mixin template Beautiful95Theme() { 18894 override Color windowBackgroundColor() { return Color(212, 212, 212); } 18895 override Color widgetBackgroundColor() { return Color.white; } 18896 override Color foregroundColor() { return Color.black; } 18897 override Color darkAccentColor() { return Color(172, 172, 172); } 18898 override Color lightAccentColor() { return Color(223, 223, 223); } 18899 override Color selectionForegroundColor() { return Color.white; } 18900 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 18901 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 18902 } 18903 18904 /// ditto 18905 mixin template DefaultLightTheme() { 18906 override Color windowBackgroundColor() { return Color(232, 232, 232); } 18907 override Color widgetBackgroundColor() { return Color.white; } 18908 override Color foregroundColor() { return Color.black; } 18909 override Color darkAccentColor() { return Color(172, 172, 172); } 18910 override Color lightAccentColor() { return Color(223, 223, 223); } 18911 override Color selectionForegroundColor() { return Color.white; } 18912 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 18913 override OperatingSystemFont defaultFont(int dpi) { 18914 version(Windows) 18915 return new OperatingSystemFont("Segoe UI"); 18916 else static if(UsingSimpledisplayCocoa) { 18917 return (new OperatingSystemFont()).loadDefault; 18918 } else { 18919 // FIXME: undo xft's scaling so we don't end up double scaled 18920 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 18921 } 18922 } 18923 } 18924 18925 /// ditto 18926 mixin template DefaultDarkTheme() { 18927 override Color windowBackgroundColor() { return Color(64, 64, 64); } 18928 override Color widgetBackgroundColor() { return Color.black; } 18929 override Color foregroundColor() { return Color.white; } 18930 override Color darkAccentColor() { return Color(20, 20, 20); } 18931 override Color lightAccentColor() { return Color(80, 80, 80); } 18932 override Color selectionForegroundColor() { return Color.white; } 18933 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 18934 override OperatingSystemFont defaultFont(int dpi) { 18935 version(Windows) 18936 return new OperatingSystemFont("Segoe UI", 12); 18937 else static if(UsingSimpledisplayCocoa) { 18938 return (new OperatingSystemFont()).loadDefault; 18939 } else { 18940 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 18941 } 18942 } 18943 } 18944 18945 /// ditto 18946 alias DefaultTheme = DefaultLightTheme; 18947 18948 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 18949 /+ 18950 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 18951 Color windowBackgroundColor() { return Color(242, 242, 242); } 18952 Color darkAccentColor() { return windowBackgroundColor; } 18953 Color lightAccentColor() { return windowBackgroundColor; } 18954 +/ 18955 } 18956 18957 /++ 18958 Event fired when an [Observable] variable changes. You will want to add an event listener referencing 18959 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 18960 18961 History: 18962 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 18963 18964 Made `final` on January 3, 2025 18965 +/ 18966 final class StateChanged(alias field) : Event { 18967 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 18968 override bool cancelable() const { return false; } 18969 this(Widget target, typeof(field) newValue) { 18970 this.newValue = newValue; 18971 super(EventString, target); 18972 } 18973 18974 typeof(field) newValue; 18975 } 18976 18977 /++ 18978 Convenience function to add a `triggered` event listener. 18979 18980 Its implementation is simply `w.addEventListener("triggered", dg);` 18981 18982 History: 18983 Added November 27, 2021 (dub v10.4) 18984 +/ 18985 void addWhenTriggered(Widget w, void delegate() dg) { 18986 w.addEventListener("triggered", dg); 18987 } 18988 18989 /++ 18990 Observable variables can be added to widgets and when they are changed, it fires 18991 off a [StateChanged] event so you can react to it. 18992 18993 It is implemented as a getter and setter property, along with another helper you 18994 can use to subscribe with is `name_changed`. You can also subscribe to the [StateChanged] 18995 event through the usual means. Just give the name of the variable. See [StateChanged] for an 18996 example. 18997 18998 To get an `ObservableReference` to the observable, use `&yourname_changed`. 18999 19000 History: 19001 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 19002 19003 As of March 5, 2025, the changed function now returns an [EventListener] handle, which 19004 you can use to disconnect the observer. 19005 +/ 19006 mixin template Observable(T, string name) { 19007 private T backing; 19008 19009 mixin(q{ 19010 EventListener } ~ name ~ q{_changed (void delegate(T) dg) { 19011 return this.addEventListener((StateChanged!this_thing ev) { 19012 dg(ev.newValue); 19013 }); 19014 } 19015 19016 @property T } ~ name ~ q{ () { 19017 return backing; 19018 } 19019 19020 @property void } ~ name ~ q{ (T t) { 19021 backing = t; 19022 auto event = new StateChanged!this_thing(this, t); 19023 event.dispatch(); 19024 } 19025 }); 19026 19027 mixin("private alias this_thing = " ~ name ~ ";"); 19028 } 19029 19030 /// ditto 19031 alias ObservableReference(T) = EventListener delegate(void delegate(T)); 19032 19033 private bool startsWith(string test, string thing) { 19034 if(test.length < thing.length) 19035 return false; 19036 return test[0 .. thing.length] == thing; 19037 } 19038 19039 private bool endsWith(string test, string thing) { 19040 if(test.length < thing.length) 19041 return false; 19042 return test[$ - thing.length .. $] == thing; 19043 } 19044 19045 /++ 19046 Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon` 19047 19048 Note they can NOT have accelerators or toolbars; those annotations will be ignored. 19049 19050 Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time. 19051 19052 See_Also: 19053 [Widget.setMenuAndToolbarFromAnnotatedCode] 19054 +/ 19055 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) { 19056 return createContextMenuFromAnnotatedCode(w, w); 19057 } 19058 19059 /// ditto 19060 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) { 19061 return createContextMenuFromAnnotatedCode_internal(w, t); 19062 } 19063 /// ditto 19064 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) { 19065 return createContextMenuFromAnnotatedCode_internal(w, t); 19066 } 19067 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) { 19068 Menu ret = new Menu("", w); 19069 19070 foreach(memberName; __traits(derivedMembers, T)) { 19071 static if(memberName != "this") 19072 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 19073 .menu menu; 19074 bool separator; 19075 .hotkey hotkey; 19076 .icon icon; 19077 string label; 19078 string tip; 19079 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 19080 static if(is(typeof(attr) == .menu)) 19081 menu = attr; 19082 else static if(is(attr == .separator)) 19083 separator = true; 19084 else static if(is(typeof(attr) == .hotkey)) 19085 hotkey = attr; 19086 else static if(is(typeof(attr) == .icon)) 19087 icon = attr; 19088 else static if(is(typeof(attr) == .label)) 19089 label = attr.label; 19090 else static if(is(typeof(attr) == .tip)) 19091 tip = attr.tip; 19092 } 19093 19094 if(menu is .menu.init) { 19095 ushort correctIcon = icon.id; // FIXME 19096 if(label.length == 0) 19097 label = memberName.toMenuLabel; 19098 19099 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName)); 19100 19101 auto action = new Action(label, correctIcon, handler); 19102 19103 if(separator) 19104 ret.addSeparator(); 19105 ret.addItem(new MenuItem(action)); 19106 } 19107 } 19108 } 19109 19110 return ret; 19111 } 19112 19113 // still do layout delegation 19114 // and... split off Window from Widget. 19115 19116 version(minigui_screenshots) 19117 struct Screenshot { 19118 string name; 19119 } 19120 19121 version(minigui_screenshots) 19122 static if(__VERSION__ > 2092) 19123 mixin(q{ 19124 shared static this() { 19125 import core.runtime; 19126 19127 static UnitTestResult screenshotMagic() { 19128 string name; 19129 19130 import arsd.png; 19131 19132 auto results = new Window(); 19133 auto button = new Button("do it", results); 19134 19135 Window.newWindowCreated = delegate(Window w) { 19136 Timer timer; 19137 timer = new Timer(250, { 19138 auto img = w.win.takeScreenshot(); 19139 timer.destroy(); 19140 19141 version(Windows) 19142 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 19143 else 19144 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 19145 19146 w.close(); 19147 }); 19148 }; 19149 19150 button.addWhenTriggered( { 19151 19152 foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) { 19153 name = null; 19154 static foreach(attr; __traits(getAttributes, test)) { 19155 static if(is(typeof(attr) == Screenshot)) 19156 name = attr.name; 19157 } 19158 if(name.length) { 19159 test(); 19160 } 19161 } 19162 19163 }); 19164 19165 results.loop(); 19166 19167 return UnitTestResult(0, 0, false, false); 19168 } 19169 19170 19171 Runtime.extendedModuleUnitTester = &screenshotMagic; 19172 } 19173 }); 19174 version(minigui_screenshots) { 19175 version(unittest) 19176 void main() {} 19177 else static assert(0, "dont forget the -unittest flag to dmd"); 19178 } 19179 19180 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 19181 // FIXME: make multiple accelerators disambiguate based ona rgs 19182 // FIXME: MainWindow ctor should have same arg order as Window 19183 // FIXME: mainwindow ctor w/ client area size instead of total size. 19184 // 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. 19185 // FIXME: tri-state checkbox 19186 // FIXME: subordinate controls grouping...