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 override string getSelectionString() { 3181 return content; 3182 } 3183 3184 version(custom_widgets) { 3185 LineEdit lineEdit; 3186 3187 override int widthStretchiness() { 3188 return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness; 3189 } 3190 override int flexBasisWidth() { 3191 return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth; 3192 } 3193 } 3194 } 3195 3196 /++ 3197 A combination of free entry with a list below it. 3198 +/ 3199 class ComboBox : ComboboxBase { 3200 this(Widget parent) { 3201 version(win32_widgets) 3202 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 3203 else version(custom_widgets) { 3204 super(parent); 3205 lineEdit = new LineEdit(this); 3206 listWidget = new ListWidget(this); 3207 listWidget.multiSelect = false; 3208 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 3209 string c = null; 3210 foreach(option; listWidget.options) 3211 if(option.selected) { 3212 c = option.label; 3213 break; 3214 } 3215 lineEdit.content = c; 3216 }); 3217 3218 listWidget.tabStop = false; 3219 this.tabStop = false; 3220 listWidget.addEventListener("focusin", &lineEdit.focus); 3221 this.addEventListener("focusin", &lineEdit.focus); 3222 3223 addDirectEventListener(EventType.change, { 3224 listWidget.setSelection(selection_); 3225 if(selection_ != -1) 3226 lineEdit.content = options[selection_]; 3227 lineEdit.focus(); 3228 redraw(); 3229 }); 3230 3231 lineEdit.addEventListener("focusin", &lineEdit.selectAll); 3232 3233 listWidget.addDirectEventListener(EventType.change, { 3234 int set = -1; 3235 foreach(idx, opt; listWidget.options) 3236 if(opt.selected) { 3237 set = cast(int) idx; 3238 break; 3239 } 3240 if(set != selection_) 3241 this.setSelection(set); 3242 }); 3243 } else static assert(false); 3244 } 3245 3246 override int minHeight() { return defaultLineHeight * 3; } 3247 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 3248 override int heightStretchiness() { return 5; } 3249 3250 version(custom_widgets) { 3251 LineEdit lineEdit; 3252 ListWidget listWidget; 3253 3254 override void addOption(string s) { 3255 listWidget.addOption(s); 3256 ComboboxBase.addOption(s); 3257 } 3258 3259 override void scrollSelectionIntoView() { 3260 listWidget.scrollSelectionIntoView(); 3261 } 3262 } 3263 } 3264 3265 /+ 3266 class Spinner : Widget { 3267 version(win32_widgets) 3268 this(Widget parent) { 3269 super(parent); 3270 parentWindow = parent.parentWindow; 3271 auto hlayout = new HorizontalLayout(this); 3272 lineEdit = new LineEdit(hlayout); 3273 upDownControl = new UpDownControl(hlayout); 3274 } 3275 3276 LineEdit lineEdit; 3277 UpDownControl upDownControl; 3278 } 3279 3280 class UpDownControl : Widget { 3281 version(win32_widgets) 3282 this(Widget parent) { 3283 super(parent); 3284 parentWindow = parent.parentWindow; 3285 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 3286 } 3287 3288 override int minHeight() { return defaultLineHeight; } 3289 override int maxHeight() { return defaultLineHeight * 3/2; } 3290 3291 override int minWidth() { return defaultLineHeight * 3/2; } 3292 override int maxWidth() { return defaultLineHeight * 3/2; } 3293 } 3294 +/ 3295 3296 /+ 3297 class DataView : Widget { 3298 // this is the omnibus data viewer 3299 // the internal data layout is something like: 3300 // string[string][] but also each node can have parents 3301 } 3302 +/ 3303 3304 3305 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 3306 3307 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 3308 3309 // FIXME: menus should prolly capture the mouse. ugh i kno. 3310 /* 3311 TextEdit needs: 3312 3313 * caret manipulation 3314 * selection control 3315 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 3316 3317 For example: 3318 3319 connect(paste, &textEdit.insertTextAtCaret); 3320 3321 would be nice. 3322 3323 3324 3325 I kinda want an omnibus dataview that combines list, tree, 3326 and table - it can be switched dynamically between them. 3327 3328 Flattening policy: only show top level, show recursive, show grouped 3329 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 3330 3331 Single select, multi select, organization, drag+drop 3332 */ 3333 3334 //static if(UsingSimpledisplayX11) 3335 version(win32_widgets) {} 3336 else version(custom_widgets) { 3337 enum scrollClickRepeatInterval = 50; 3338 3339 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 3340 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 3341 enum activeTabColor = lightAccentColor; 3342 enum hoveringColor = Color(228, 228, 228); 3343 enum buttonColor = windowBackgroundColor; 3344 enum depressedButtonColor = darkAccentColor; 3345 enum activeListXorColor = Color(255, 255, 127); 3346 enum progressBarColor = Color(0, 0, 128); 3347 enum activeMenuItemColor = Color(0, 0, 128); 3348 3349 }} 3350 else static assert(false); 3351 deprecated("Get these properties off the `visualTheme` instead.") { 3352 // these are used by horizontal rule so not just custom_widgets. for now at least. 3353 enum darkAccentColor = Color(172, 172, 172); 3354 enum lightAccentColor = Color(223, 223, 223); // used to be 223 3355 } 3356 3357 private const(wchar)* toWstringzInternal(in char[] s) { 3358 wchar[] str; 3359 str.reserve(s.length + 1); 3360 foreach(dchar ch; s) 3361 str ~= ch; 3362 str ~= '\0'; 3363 return str.ptr; 3364 } 3365 3366 static if(SimpledisplayTimerAvailable) 3367 void setClickRepeat(Widget w, int interval, int delay = 250) { 3368 Timer timer; 3369 int delayRemaining = delay / interval; 3370 if(delayRemaining <= 1) 3371 delayRemaining = 2; 3372 3373 immutable originalDelayRemaining = delayRemaining; 3374 3375 w.addDirectEventListener((scope MouseDownEvent ev) { 3376 if(ev.srcElement !is w) 3377 return; 3378 if(timer !is null) { 3379 timer.destroy(); 3380 timer = null; 3381 } 3382 delayRemaining = originalDelayRemaining; 3383 timer = new Timer(interval, () { 3384 if(delayRemaining > 0) 3385 delayRemaining--; 3386 else { 3387 auto ev = new Event("triggered", w); 3388 ev.sendDirectly(); 3389 } 3390 }); 3391 }); 3392 3393 w.addDirectEventListener((scope MouseUpEvent ev) { 3394 if(ev.srcElement !is w) 3395 return; 3396 if(timer !is null) { 3397 timer.destroy(); 3398 timer = null; 3399 } 3400 }); 3401 3402 w.addDirectEventListener((scope MouseLeaveEvent ev) { 3403 if(ev.srcElement !is w) 3404 return; 3405 if(timer !is null) { 3406 timer.destroy(); 3407 timer = null; 3408 } 3409 }); 3410 3411 } 3412 else 3413 void setClickRepeat(Widget w, int interval, int delay = 250) {} 3414 3415 enum FrameStyle { 3416 none, /// 3417 risen, /// a 3d pop-out effect (think Windows 95 button) 3418 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 3419 solid, /// 3420 dotted, /// 3421 fantasy, /// a style based on a popular fantasy video game 3422 rounded, /// a rounded rectangle 3423 } 3424 3425 version(custom_widgets) 3426 deprecated 3427 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 3428 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3429 } 3430 3431 version(custom_widgets) 3432 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 3433 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 3434 } 3435 3436 version(custom_widgets) 3437 deprecated 3438 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 3439 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3440 } 3441 3442 int getBorderWidth(FrameStyle style) { 3443 final switch(style) { 3444 case FrameStyle.sunk, FrameStyle.risen: 3445 return 2; 3446 case FrameStyle.none: 3447 return 0; 3448 case FrameStyle.solid: 3449 return 1; 3450 case FrameStyle.dotted: 3451 return 1; 3452 case FrameStyle.fantasy: 3453 return 3; 3454 case FrameStyle.rounded: 3455 return 2; 3456 } 3457 } 3458 3459 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 3460 int borderWidth = getBorderWidth(style); 3461 final switch(style) { 3462 case FrameStyle.sunk, FrameStyle.risen: 3463 // outer layer 3464 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 3465 break; 3466 case FrameStyle.none: 3467 painter.outlineColor = background; 3468 break; 3469 case FrameStyle.solid: 3470 case FrameStyle.rounded: 3471 painter.pen = Pen(border, 1); 3472 break; 3473 case FrameStyle.dotted: 3474 painter.pen = Pen(border, 1, Pen.Style.Dotted); 3475 break; 3476 case FrameStyle.fantasy: 3477 painter.pen = Pen(border, 3); 3478 break; 3479 } 3480 3481 painter.fillColor = background; 3482 3483 if(style == FrameStyle.rounded) { 3484 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 3485 } else { 3486 painter.drawRectangle(Point(x + 0, y + 0), width, height); 3487 3488 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 3489 // 3d effect 3490 auto vt = WidgetPainter.visualTheme; 3491 3492 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 3493 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 3494 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 3495 3496 // inner layer 3497 //right, bottom 3498 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 3499 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 3500 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 3501 // left, top 3502 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 3503 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 3504 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 3505 } else if(style == FrameStyle.fantasy) { 3506 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 3507 painter.fillColor = Color.transparent; 3508 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 3509 } 3510 } 3511 3512 return borderWidth; 3513 } 3514 3515 /++ 3516 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. 3517 3518 See_Also: 3519 [MenuItem] 3520 [ToolButton] 3521 [Menu.addItem] 3522 +/ 3523 class Action { 3524 version(win32_widgets) { 3525 private int id; 3526 private static int lastId = 9000; 3527 private static Action[int] mapping; 3528 } 3529 3530 KeyEvent accelerator; 3531 3532 // FIXME: disable message 3533 // and toggle thing? 3534 // ??? and trigger arguments too ??? 3535 3536 /++ 3537 Params: 3538 label = the textual label 3539 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 3540 triggered = initial handler, more can be added via the [triggered] member. 3541 +/ 3542 this(string label, ushort icon = 0, void delegate() triggered = null) { 3543 this.label = label; 3544 this.iconId = icon; 3545 if(triggered !is null) 3546 this.triggered ~= triggered; 3547 version(win32_widgets) { 3548 id = ++lastId; 3549 mapping[id] = this; 3550 } 3551 } 3552 3553 private string label; 3554 private ushort iconId; 3555 // icon 3556 3557 // when it is triggered, the triggered event is fired on the window 3558 /// The list of handlers when it is triggered. 3559 void delegate()[] triggered; 3560 } 3561 3562 /* 3563 plan: 3564 keyboard accelerators 3565 3566 * menus (and popups and tooltips) 3567 * status bar 3568 * toolbars and buttons 3569 3570 sortable table view 3571 3572 maybe notification area icons 3573 basic clipboard 3574 3575 * radio box 3576 splitter 3577 toggle buttons (optionally mutually exclusive, like in Paint) 3578 label, rich text display, multi line plain text (selectable) 3579 * fieldset 3580 * nestable grid layout 3581 single line text input 3582 * multi line text input 3583 slider 3584 spinner 3585 list box 3586 drop down 3587 combo box 3588 auto complete box 3589 * progress bar 3590 3591 terminal window/widget (on unix it might even be a pty but really idk) 3592 3593 ok button 3594 cancel button 3595 3596 keyboard hotkeys 3597 3598 scroll widget 3599 3600 event redirections and network transparency 3601 script integration 3602 */ 3603 3604 3605 /* 3606 MENUS 3607 3608 auto bar = new MenuBar(window); 3609 window.menuBar = bar; 3610 3611 auto fileMenu = bar.addItem(new Menu("&File")); 3612 fileMenu.addItem(new MenuItem("&Exit")); 3613 3614 3615 EVENTS 3616 3617 For controls, you should usually use "triggered" rather than "click", etc., because 3618 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3619 This is the case on menus and pushbuttons. 3620 3621 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3622 */ 3623 3624 3625 /* 3626 enum LinePreference { 3627 AlwaysOnOwnLine, // always on its own line 3628 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3629 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 3630 } 3631 */ 3632 3633 /++ 3634 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. 3635 3636 --- 3637 class MyWidget : Widget { 3638 this(Widget parent) { super(parent); } 3639 3640 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3641 mixin Padding!q{4}; 3642 3643 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3644 mixin Margin!q{8}; 3645 3646 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3647 // while Top/Bottom/Right remain 8 from the mixin above. 3648 override int marginLeft() { return 2; } 3649 } 3650 --- 3651 3652 3653 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]). 3654 3655 Padding is the area inside a widget where its background is drawn, but the content avoids. 3656 3657 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!). 3658 3659 * 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. 3660 +/ 3661 mixin template Padding(string code) { 3662 override int paddingLeft() { return mixin(code);} 3663 override int paddingRight() { return mixin(code);} 3664 override int paddingTop() { return mixin(code);} 3665 override int paddingBottom() { return mixin(code);} 3666 } 3667 3668 /// ditto 3669 mixin template Margin(string code) { 3670 override int marginLeft() { return mixin(code);} 3671 override int marginRight() { return mixin(code);} 3672 override int marginTop() { return mixin(code);} 3673 override int marginBottom() { return mixin(code);} 3674 } 3675 3676 private 3677 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3678 enum calcingV = relevantMeasure == "height"; 3679 3680 parent.registerMovement(); 3681 3682 if(parent.children.length == 0) 3683 return; 3684 3685 auto parentStyle = parent.getComputedStyle(); 3686 3687 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3688 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3689 3690 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3691 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3692 3693 // my own width and height should already be set by the caller of this function... 3694 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3695 mixin("parentStyle.padding"~firstThingy~"()") - 3696 mixin("parentStyle.padding"~secondThingy~"()"); 3697 3698 int stretchinessSum; 3699 int stretchyChildSum; 3700 int lastMargin = 0; 3701 3702 int shrinkinessSum; 3703 int shrinkyChildSum; 3704 3705 // set initial size 3706 foreach(child; parent.children) { 3707 3708 auto childStyle = child.getComputedStyle(); 3709 3710 if(cast(StaticPosition) child) 3711 continue; 3712 if(child.hidden) 3713 continue; 3714 3715 const iw = child.flexBasisWidth(); 3716 const ih = child.flexBasisHeight(); 3717 3718 static if(calcingV) { 3719 child.width = parent.width - 3720 mixin("childStyle.margin"~otherFirstThingy~"()") - 3721 mixin("childStyle.margin"~otherSecondThingy~"()") - 3722 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3723 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3724 3725 if(child.width < 0) 3726 child.width = 0; 3727 if(child.width > childStyle.maxWidth()) 3728 child.width = childStyle.maxWidth(); 3729 3730 if(iw > 0) { 3731 auto totalPossible = child.width; 3732 if(child.width > iw && child.widthStretchiness() == 0) 3733 child.width = iw; 3734 } 3735 3736 child.height = mymax(childStyle.minHeight(), ih); 3737 } else { 3738 // set to take all the space 3739 child.height = parent.height - 3740 mixin("childStyle.margin"~firstThingy~"()") - 3741 mixin("childStyle.margin"~secondThingy~"()") - 3742 mixin("parentStyle.padding"~firstThingy~"()") - 3743 mixin("parentStyle.padding"~secondThingy~"()"); 3744 3745 // then clamp it 3746 if(child.height < 0) 3747 child.height = 0; 3748 if(child.height > childStyle.maxHeight()) 3749 child.height = childStyle.maxHeight(); 3750 3751 // and if possible, respect the ideal target 3752 if(ih > 0) { 3753 auto totalPossible = child.height; 3754 if(child.height > ih && child.heightStretchiness() == 0) 3755 child.height = ih; 3756 } 3757 3758 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3759 child.width = mymax(childStyle.minWidth(), iw); 3760 } 3761 3762 spaceRemaining -= mixin("child." ~ relevantMeasure); 3763 3764 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3765 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3766 lastMargin = margin; 3767 spaceRemaining -= thisMargin + margin; 3768 3769 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3770 stretchinessSum += s; 3771 if(s > 0) 3772 stretchyChildSum++; 3773 3774 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3775 shrinkinessSum += s2; 3776 if(s2 > 0) 3777 shrinkyChildSum++; 3778 } 3779 3780 if(spaceRemaining < 0 && shrinkyChildSum) { 3781 // shrink to get into the space if it is possible 3782 auto toRemove = -spaceRemaining; 3783 auto removalPerItem = toRemove / shrinkinessSum; 3784 auto remainder = toRemove % shrinkinessSum; 3785 3786 // FIXME: wtf why am i shrinking things with no shrinkiness? 3787 3788 foreach(child; parent.children) { 3789 auto childStyle = child.getComputedStyle(); 3790 if(cast(StaticPosition) child) 3791 continue; 3792 if(child.hidden) 3793 continue; 3794 static if(calcingV) { 3795 auto minimum = childStyle.minHeight(); 3796 auto stretch = childStyle.heightShrinkiness(); 3797 } else { 3798 auto minimum = childStyle.minWidth(); 3799 auto stretch = childStyle.widthShrinkiness(); 3800 } 3801 3802 if(mixin("child._" ~ relevantMeasure) <= minimum) 3803 continue; 3804 // import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch); 3805 3806 mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh. 3807 3808 spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum; 3809 } 3810 } 3811 3812 // stretch to fill space 3813 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3814 auto spacePerChild = spaceRemaining / stretchinessSum; 3815 bool spreadEvenly; 3816 bool giveToBiggest; 3817 if(spacePerChild <= 0) { 3818 spacePerChild = spaceRemaining / stretchyChildSum; 3819 spreadEvenly = true; 3820 } 3821 if(spacePerChild <= 0) { 3822 giveToBiggest = true; 3823 } 3824 int previousSpaceRemaining = spaceRemaining; 3825 stretchinessSum = 0; 3826 Widget mostStretchy; 3827 int mostStretchyS; 3828 foreach(child; parent.children) { 3829 auto childStyle = child.getComputedStyle(); 3830 if(cast(StaticPosition) child) 3831 continue; 3832 if(child.hidden) 3833 continue; 3834 static if(calcingV) { 3835 auto maximum = childStyle.maxHeight(); 3836 } else { 3837 auto maximum = childStyle.maxWidth(); 3838 } 3839 3840 if(mixin("child." ~ relevantMeasure) >= maximum) { 3841 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3842 mixin("child._" ~ relevantMeasure) -= adj; 3843 spaceRemaining += adj; 3844 continue; 3845 } 3846 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3847 if(s <= 0) 3848 continue; 3849 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3850 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3851 spaceRemaining -= spaceAdjustment; 3852 if(mixin("child." ~ relevantMeasure) > maximum) { 3853 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3854 mixin("child._" ~ relevantMeasure) -= diff; 3855 spaceRemaining += diff; 3856 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3857 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3858 if(mostStretchy is null || s >= mostStretchyS) { 3859 mostStretchy = child; 3860 mostStretchyS = s; 3861 } 3862 } 3863 } 3864 3865 if(giveToBiggest && mostStretchy !is null) { 3866 auto child = mostStretchy; 3867 auto childStyle = child.getComputedStyle(); 3868 int spaceAdjustment = spaceRemaining; 3869 3870 static if(calcingV) 3871 auto maximum = childStyle.maxHeight(); 3872 else 3873 auto maximum = childStyle.maxWidth(); 3874 3875 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3876 spaceRemaining -= spaceAdjustment; 3877 if(mixin("child._" ~ relevantMeasure) > maximum) { 3878 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3879 mixin("child._" ~ relevantMeasure) -= diff; 3880 spaceRemaining += diff; 3881 } 3882 } 3883 3884 if(spaceRemaining == previousSpaceRemaining) { 3885 if(mostStretchy !is null) { 3886 static if(calcingV) 3887 auto maximum = mostStretchy.maxHeight(); 3888 else 3889 auto maximum = mostStretchy.maxWidth(); 3890 3891 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3892 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3893 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3894 } 3895 break; // apparently nothing more we can do 3896 } 3897 } 3898 3899 foreach(child; parent.children) { 3900 auto childStyle = child.getComputedStyle(); 3901 if(cast(StaticPosition) child) 3902 continue; 3903 if(child.hidden) 3904 continue; 3905 3906 static if(calcingV) 3907 auto maximum = childStyle.maxHeight(); 3908 else 3909 auto maximum = childStyle.maxWidth(); 3910 if(mixin("child._" ~ relevantMeasure) > maximum) 3911 mixin("child._" ~ relevantMeasure) = maximum; 3912 } 3913 3914 // position 3915 lastMargin = 0; 3916 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3917 foreach(child; parent.children) { 3918 auto childStyle = child.getComputedStyle(); 3919 if(cast(StaticPosition) child) { 3920 child.recomputeChildLayout(); 3921 continue; 3922 } 3923 if(child.hidden) 3924 continue; 3925 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3926 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3927 currentPos += thisMargin; 3928 static if(calcingV) { 3929 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3930 child.y = currentPos; 3931 } else { 3932 child.x = currentPos; 3933 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3934 3935 } 3936 currentPos += mixin("child." ~ relevantMeasure); 3937 currentPos += margin; 3938 lastMargin = margin; 3939 3940 child.recomputeChildLayout(); 3941 } 3942 } 3943 3944 int mymax(int a, int b) { return a > b ? a : b; } 3945 int mymax(int a, int b, int c) { 3946 auto d = mymax(a, b); 3947 return c > d ? c : d; 3948 } 3949 3950 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3951 // and here, it must be integrable with the layout, the event system, and not be painted over. 3952 version(win32_widgets) { 3953 3954 // this function just does stuff that a parent window needs for redirection 3955 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3956 this_.hookedWndProc(msg, wParam, lParam); 3957 3958 switch(msg) { 3959 3960 case WM_VSCROLL, WM_HSCROLL: 3961 auto pos = HIWORD(wParam); 3962 auto m = LOWORD(wParam); 3963 3964 auto scrollbarHwnd = cast(HWND) lParam; 3965 3966 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3967 3968 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3969 3970 switch(m) { 3971 /+ 3972 // I don't think those messages are ever actually sent normally by the widget itself, 3973 // they are more used for the keyboard interface. methinks. 3974 case SB_BOTTOM: 3975 // writeln("end"); 3976 auto event = new Event("scrolltoend", *widgetp); 3977 event.dispatch(); 3978 //if(!event.defaultPrevented) 3979 break; 3980 case SB_TOP: 3981 // writeln("top"); 3982 auto event = new Event("scrolltobeginning", *widgetp); 3983 event.dispatch(); 3984 break; 3985 case SB_ENDSCROLL: 3986 // idk 3987 break; 3988 +/ 3989 case SB_LINEDOWN: 3990 (*widgetp).emitCommand!"scrolltonextline"(); 3991 return 0; 3992 case SB_LINEUP: 3993 (*widgetp).emitCommand!"scrolltopreviousline"(); 3994 return 0; 3995 case SB_PAGEDOWN: 3996 (*widgetp).emitCommand!"scrolltonextpage"(); 3997 return 0; 3998 case SB_PAGEUP: 3999 (*widgetp).emitCommand!"scrolltopreviouspage"(); 4000 return 0; 4001 case SB_THUMBPOSITION: 4002 auto ev = new ScrollToPositionEvent(*widgetp, pos); 4003 ev.dispatch(); 4004 return 0; 4005 case SB_THUMBTRACK: 4006 // eh kinda lying but i like the real time update display 4007 auto ev = new ScrollToPositionEvent(*widgetp, pos); 4008 ev.dispatch(); 4009 4010 // the event loop doesn't seem to carry on with a requested redraw.. 4011 // so we request it to get our dirty bit set... 4012 // then we need to immediately actually redraw it too for instant feedback to user 4013 SimpleWindow.processAllCustomEvents(); 4014 SimpleWindow.processAllCustomEvents(); 4015 //if(this_.parentWindow) 4016 //this_.parentWindow.actualRedraw(); 4017 4018 // and this ensures the WM_PAINT message is sent fairly quickly 4019 // still seems to lag a little in large windows but meh it basically works. 4020 if(this_.parentWindow) { 4021 // FIXME: if painting is slow, this does still lag 4022 // we probably will want to expose some user hook to ScrollWindowEx 4023 // or something. 4024 UpdateWindow(this_.parentWindow.hwnd); 4025 } 4026 return 0; 4027 default: 4028 } 4029 } 4030 break; 4031 4032 case WM_CONTEXTMENU: 4033 auto hwndFrom = cast(HWND) wParam; 4034 4035 auto xPos = cast(short) LOWORD(lParam); 4036 auto yPos = cast(short) HIWORD(lParam); 4037 4038 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4039 POINT p; 4040 p.x = xPos; 4041 p.y = yPos; 4042 ScreenToClient(hwnd, &p); 4043 auto clientX = cast(ushort) p.x; 4044 auto clientY = cast(ushort) p.y; 4045 4046 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 4047 4048 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 4049 return 0; 4050 } 4051 } 4052 break; 4053 4054 case WM_DRAWITEM: 4055 auto dis = cast(DRAWITEMSTRUCT*) lParam; 4056 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 4057 return (*widgetp).handleWmDrawItem(dis); 4058 } 4059 break; 4060 4061 case WM_NOTIFY: 4062 auto hdr = cast(NMHDR*) lParam; 4063 auto hwndFrom = hdr.hwndFrom; 4064 auto code = hdr.code; 4065 4066 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4067 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 4068 } 4069 break; 4070 case WM_COMMAND: 4071 auto handle = cast(HWND) lParam; 4072 auto cmd = HIWORD(wParam); 4073 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 4074 4075 default: 4076 // pass it on 4077 } 4078 return 0; 4079 } 4080 4081 4082 4083 extern(Windows) 4084 private 4085 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 4086 // but can i merge them?! 4087 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4088 // try { writeln(iMessage); } catch(Exception e) {}; 4089 4090 if(auto te = hWnd in Widget.nativeMapping) { 4091 try { 4092 4093 te.hookedWndProc(iMessage, wParam, lParam); 4094 4095 int mustReturn; 4096 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 4097 if(mustReturn) 4098 return ret; 4099 4100 if(iMessage == WM_SETFOCUS) { 4101 auto lol = *te; 4102 while(lol !is null && lol.implicitlyCreated) 4103 lol = lol.parent; 4104 lol.focus(); 4105 //(*te).parentWindow.focusedWidget = lol; 4106 } 4107 4108 4109 if(iMessage == WM_CTLCOLOREDIT) { 4110 4111 } 4112 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 4113 SetBkMode(cast(HDC) wParam, TRANSPARENT); 4114 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 4115 //GetStockObject(NULL_BRUSH); 4116 } 4117 4118 auto pos = getChildPositionRelativeToParentOrigin(*te); 4119 lastDefaultPrevented = false; 4120 // try { writeln(typeid(*te)); } catch(Exception e) {} 4121 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 4122 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 4123 else { 4124 // it was something we recognized, should only call the window procedure if the default was not prevented 4125 } 4126 } catch(Exception e) { 4127 assert(0, e.toString()); 4128 } 4129 return 0; 4130 } 4131 assert(0, "shouldn't be receiving messages for this window...."); 4132 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 4133 } 4134 4135 extern(Windows) 4136 private 4137 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 4138 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4139 if(iMessage == WM_ERASEBKGND) { 4140 auto dc = GetDC(hWnd); 4141 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 4142 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 4143 RECT r; 4144 GetWindowRect(hWnd, &r); 4145 // since the pen is null, to fill the whole space, we need the +1 on both. 4146 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 4147 SelectObject(dc, p); 4148 SelectObject(dc, b); 4149 ReleaseDC(hWnd, dc); 4150 InvalidateRect(hWnd, null, false); // redraw the border 4151 return 1; 4152 } 4153 return HookedWndProc(hWnd, iMessage, wParam, lParam); 4154 } 4155 4156 /++ 4157 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 4158 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 4159 of minigui's expectations. 4160 4161 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 4162 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 4163 4164 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 4165 4166 To check if you can use this, use `static if(UsingWin32Widgets)`. 4167 +/ 4168 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 4169 assert(p.parentWindow !is null); 4170 assert(p.parentWindow.win.impl.hwnd !is null); 4171 4172 auto bsgroupbox = style == BS_GROUPBOX; 4173 4174 HWND phwnd; 4175 4176 auto wtf = p.parent; 4177 while(wtf) { 4178 if(wtf.hwnd !is null) { 4179 phwnd = wtf.hwnd; 4180 break; 4181 } 4182 wtf = wtf.parent; 4183 } 4184 4185 if(phwnd is null) 4186 phwnd = p.parentWindow.win.impl.hwnd; 4187 4188 assert(phwnd !is null); 4189 4190 WCharzBuffer wt = WCharzBuffer(windowText); 4191 4192 style |= WS_VISIBLE | WS_CHILD; 4193 //if(className != WC_TABCONTROL) 4194 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 4195 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 4196 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 4197 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 4198 4199 assert(p.hwnd !is null); 4200 4201 4202 static HFONT font; 4203 if(font is null) { 4204 NONCLIENTMETRICS params; 4205 params.cbSize = params.sizeof; 4206 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 4207 font = CreateFontIndirect(¶ms.lfMessageFont); 4208 } 4209 } 4210 4211 if(font) 4212 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 4213 4214 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 4215 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 4216 Widget.nativeMapping[p.hwnd] = p; 4217 4218 if(bsgroupbox) 4219 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 4220 else 4221 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4222 4223 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 4224 4225 p.registerMovement(); 4226 } 4227 } 4228 4229 version(win32_widgets) 4230 private 4231 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 4232 if(hwnd is null || hwnd in Widget.nativeMapping) 4233 return true; 4234 auto parent = cast(Widget) cast(void*) lparam; 4235 Widget p = new Widget(null); 4236 p._parent = parent; 4237 p.parentWindow = parent.parentWindow; 4238 p.hwnd = hwnd; 4239 p.implicitlyCreated = true; 4240 Widget.nativeMapping[p.hwnd] = p; 4241 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4242 return true; 4243 } 4244 4245 /++ 4246 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 4247 +/ 4248 struct WidgetPainter { 4249 this(ScreenPainter screenPainter, Widget drawingUpon) { 4250 this.drawingUpon = drawingUpon; 4251 this.screenPainter = screenPainter; 4252 4253 this.widgetClipRectangle = screenPainter.currentClipRectangle; 4254 4255 // this.screenPainter.impl.enableXftDraw(); 4256 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4257 this.screenPainter.setFont(font); 4258 } 4259 4260 /++ 4261 EXPERIMENTAL. subject to change. 4262 4263 When you draw a cursor, you can draw this to notify your window of where it is, 4264 for IME systems to use. 4265 +/ 4266 void notifyCursorPosition(int x, int y, int width, int height) { 4267 if(auto a = drawingUpon.parentWindow) 4268 if(auto w = a.inputProxy) { 4269 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 4270 } 4271 } 4272 4273 private Rectangle widgetClipRectangle; 4274 4275 private Rectangle setClipRectangleForWidget(Point upperLeft, int width, int height) { 4276 widgetClipRectangle = Rectangle(upperLeft, Size(width, height)); 4277 4278 return screenPainter.setClipRectangle(widgetClipRectangle); 4279 } 4280 4281 /++ 4282 Sets the clip rectangle to the given settings. It will automatically calculate the intersection 4283 of your widget's content boundaries and your requested clip rectangle. 4284 4285 History: 4286 Before February 26, 2025, you could sometimes exceed widget boundaries, as this forwarded 4287 directly to the underlying `ScreenPainter`. It now wraps it to calculate the intersection. 4288 +/ 4289 Rectangle setClipRectangle(Rectangle rectangle) { 4290 return screenPainter.setClipRectangle(rectangle.intersectionOf(widgetClipRectangle)); 4291 } 4292 /// ditto 4293 Rectangle setClipRectangle(Point upperLeft, int width, int height) { 4294 return setClipRectangle(Rectangle(upperLeft, Size(width, height))); 4295 } 4296 /// ditto 4297 Rectangle setClipRectangle(Point upperLeft, Size size) { 4298 return setClipRectangle(Rectangle(upperLeft, size)); 4299 } 4300 4301 /// 4302 ScreenPainter screenPainter; 4303 /// Forward to the screen painter for all other methods, see [arsd.simpledisplay.ScreenPainter] for more information 4304 alias screenPainter this; 4305 4306 private Widget drawingUpon; 4307 4308 /++ 4309 This is the list of rectangles that actually need to be redrawn. 4310 4311 Not actually implemented yet. 4312 +/ 4313 Rectangle[] invalidatedRectangles; 4314 4315 private static BaseVisualTheme _visualTheme; 4316 4317 /++ 4318 Functions to access the visual theme and helpers to easily use it. 4319 4320 These are aware of the current widget's computed style out of the theme. 4321 +/ 4322 static @property BaseVisualTheme visualTheme() { 4323 if(_visualTheme is null) 4324 _visualTheme = new DefaultVisualTheme(); 4325 return _visualTheme; 4326 } 4327 4328 /// ditto 4329 static @property void visualTheme(BaseVisualTheme theme) { 4330 _visualTheme = theme; 4331 4332 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 4333 } 4334 4335 /// ditto 4336 Color themeForeground() { 4337 return drawingUpon.getComputedStyle().foregroundColor(); 4338 } 4339 4340 /// ditto 4341 Color themeBackground() { 4342 return drawingUpon.getComputedStyle().background.color; 4343 } 4344 4345 int isDarkTheme() { 4346 return 0; // unspecified, yes, no as enum. FIXME 4347 } 4348 4349 /++ 4350 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. 4351 4352 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 4353 4354 If you change teh clip rectangle, you should change it back before you return. 4355 4356 4357 The sequence it uses is: 4358 background 4359 content (delegated to you) 4360 border 4361 focused outline 4362 selected overlay 4363 4364 Example code: 4365 4366 --- 4367 void paint(WidgetPainter painter) { 4368 painter.drawThemed((bounds) { 4369 return bounds; // if the selection overlay should be contained, you can return it here. 4370 }); 4371 } 4372 --- 4373 +/ 4374 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 4375 drawThemed((WidgetPainter painter, const Rectangle bounds) { 4376 return drawBody(bounds); 4377 }); 4378 } 4379 // this overload is actually mroe for setting the delegate to a virtual function 4380 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 4381 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 4382 4383 auto cs = drawingUpon.getComputedStyle(); 4384 4385 auto bg = cs.background.color; 4386 4387 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 4388 4389 rect.left += borderWidth; 4390 rect.right -= borderWidth; 4391 rect.top += borderWidth; 4392 rect.bottom -= borderWidth; 4393 4394 auto insideBorderRect = rect; 4395 4396 rect.left += cs.paddingLeft; 4397 rect.right -= cs.paddingRight; 4398 rect.top += cs.paddingTop; 4399 rect.bottom -= cs.paddingBottom; 4400 4401 this.outlineColor = this.themeForeground; 4402 this.fillColor = bg; 4403 4404 auto widgetFont = cs.fontCached; 4405 if(widgetFont !is null) 4406 this.setFont(widgetFont); 4407 4408 rect = drawBody(this, rect); 4409 4410 if(widgetFont !is null) { 4411 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4412 this.setFont(vtFont); 4413 else 4414 this.setFont(null); 4415 } 4416 4417 if(auto os = cs.outlineStyle()) { 4418 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 4419 this.fillColor = Color.transparent; 4420 this.drawRectangle(insideBorderRect); 4421 } 4422 } 4423 4424 /++ 4425 First, draw the background. 4426 Then draw your content. 4427 Next, draw the border. 4428 And the focused indicator. 4429 And the is-selected box. 4430 4431 If it is focused i can draw the outline too... 4432 4433 If selected i can even do the xor action but that's at the end. 4434 +/ 4435 void drawThemeBackground() { 4436 4437 } 4438 4439 void drawThemeBorder() { 4440 4441 } 4442 4443 // all this stuff is a dangerous experiment.... 4444 static class ScriptableVersion { 4445 ScreenPainterImplementation* p; 4446 int originX, originY; 4447 4448 @scriptable: 4449 void drawRectangle(int x, int y, int width, int height) { 4450 p.drawRectangle(x + originX, y + originY, width, height); 4451 } 4452 void drawLine(int x1, int y1, int x2, int y2) { 4453 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 4454 } 4455 void drawText(int x, int y, string text) { 4456 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 4457 } 4458 void setOutlineColor(int r, int g, int b) { 4459 p.pen = Pen(Color(r,g,b), 1); 4460 } 4461 void setFillColor(int r, int g, int b) { 4462 p.fillColor = Color(r,g,b); 4463 } 4464 } 4465 4466 ScriptableVersion toArsdJsvar() { 4467 auto sv = new ScriptableVersion; 4468 sv.p = this.screenPainter.impl; 4469 sv.originX = this.screenPainter.originX; 4470 sv.originY = this.screenPainter.originY; 4471 return sv; 4472 } 4473 4474 static WidgetPainter fromJsVar(T)(T t) { 4475 return WidgetPainter.init; 4476 } 4477 // done.......... 4478 } 4479 4480 4481 struct Style { 4482 static struct helper(string m, T) { 4483 enum method = m; 4484 T v; 4485 4486 mixin template MethodOverride(typeof(this) v) { 4487 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 4488 } 4489 } 4490 4491 static auto opDispatch(string method, T)(T value) { 4492 return helper!(method, T)(value); 4493 } 4494 } 4495 4496 /++ 4497 Implementation detail of the [ControlledBy] UDA. 4498 4499 History: 4500 Added Oct 28, 2020 4501 +/ 4502 struct ControlledBy_(T, Args...) { 4503 Args args; 4504 4505 static if(Args.length) 4506 this(Args args) { 4507 this.args = args; 4508 } 4509 4510 private T construct(Widget parent) { 4511 return new T(args, parent); 4512 } 4513 } 4514 4515 /++ 4516 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 4517 4518 History: 4519 Added Oct 28, 2020 4520 +/ 4521 auto ControlledBy(T, Args...)(Args args) { 4522 return ControlledBy_!(T, Args)(args); 4523 } 4524 4525 struct ContainerMeta { 4526 string name; 4527 ContainerMeta[] children; 4528 Widget function(Widget parent) factory; 4529 4530 Widget instantiate(Widget parent) { 4531 auto n = factory(parent); 4532 n.name = name; 4533 foreach(child; children) 4534 child.instantiate(n); 4535 return n; 4536 } 4537 } 4538 4539 /++ 4540 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 4541 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 4542 4543 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 4544 structures. It works fine on structs declared inside functions though. 4545 4546 See: https://issues.dlang.org/show_bug.cgi?id=21984 4547 +/ 4548 template Container(CArgs...) { 4549 static if(CArgs.length && is(CArgs[0] : Widget)) { 4550 private alias Super = CArgs[0]; 4551 private alias CArgs2 = CArgs[1 .. $]; 4552 } else { 4553 private alias Super = Layout; 4554 private alias CArgs2 = CArgs; 4555 } 4556 4557 class Container : Super { 4558 this(Widget parent) { super(parent); } 4559 4560 // just to partially support old gdc versions 4561 version(GNU) { 4562 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 4563 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 4564 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 4565 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 4566 } else mixin(q{ 4567 static foreach(Arg; CArgs2) { 4568 mixin Arg.MethodOverride!(Arg); 4569 } 4570 }); 4571 4572 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 4573 return ContainerMeta( 4574 name, 4575 children.dup, 4576 function (Widget parent) { return new typeof(this)(parent); } 4577 ); 4578 } 4579 4580 static ContainerMeta opCall(ContainerMeta[] children...) { 4581 return opCall(null, children); 4582 } 4583 } 4584 } 4585 4586 /++ 4587 The data controller widget is created by reflecting over the given 4588 data type. You can use [ControlledBy] as a UDA on a struct or 4589 just let it create things automatically. 4590 4591 Unlike [dialog], this uses real-time updating of the data and 4592 you add it to another window yourself. 4593 4594 --- 4595 struct Test { 4596 int x; 4597 int y; 4598 } 4599 4600 auto window = new Window(); 4601 auto dcw = new DataControllerWidget!Test(new Test, window); 4602 --- 4603 4604 The way it works is any public members are given a widget based 4605 on their data type, and public methods trigger an action button 4606 if no relevant parameters or a dialog action if it does have 4607 parameters, similar to the [menu] facility. 4608 4609 If you change data programmatically, without going through the 4610 DataControllerWidget methods, you will have to tell it something 4611 has changed and it needs to redraw. This is done with the `invalidate` 4612 method. 4613 4614 History: 4615 Added Oct 28, 2020 4616 +/ 4617 /// Group: generating_from_code 4618 class DataControllerWidget(T) : WidgetContainer { 4619 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 4620 private alias Tref = T; 4621 else 4622 private alias Tref = T*; 4623 4624 Tref datum; 4625 4626 /++ 4627 See_also: [addDataControllerWidget] 4628 +/ 4629 this(Tref datum, Widget parent) { 4630 this.datum = datum; 4631 4632 Widget cp = this; 4633 4634 super(parent); 4635 4636 foreach(attr; __traits(getAttributes, T)) 4637 static if(is(typeof(attr) == ContainerMeta)) { 4638 cp = attr.instantiate(this); 4639 } 4640 4641 auto def = this.getByName("default"); 4642 if(def !is null) 4643 cp = def; 4644 4645 Widget helper(string name) { 4646 auto maybe = this.getByName(name); 4647 if(maybe is null) 4648 return cp; 4649 return maybe; 4650 4651 } 4652 4653 foreach(member; __traits(allMembers, T)) 4654 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4655 static if(is(typeof(__traits(getMember, this.datum, member)))) 4656 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4657 void delegate() updateWidgetFromData; 4658 void delegate() updateDataFromWidget; 4659 4660 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4661 auto w = widgetFor!(__traits(getMember, T, member), void)(null, helper(member), updateWidgetFromData, updateDataFromWidget); 4662 w.addEventListener("triggered", delegate() { 4663 makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))(); 4664 notifyDataUpdated(); 4665 }); 4666 } else { 4667 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), updateWidgetFromData, updateDataFromWidget); 4668 if(updateWidgetFromData) 4669 updaters ~= updateWidgetFromData; 4670 if(updateDataFromWidget) 4671 w.addEventListener(EventType.change, (Event ev) { 4672 updateDataFromWidget(); 4673 }); 4674 } 4675 /+ 4676 static if(is(typeof(w.isChecked) == bool)) { 4677 __traits(getMember, this.datum, member) = w.isChecked; 4678 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4679 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4680 } else static if(is(typeof(w.value) == int)) { 4681 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4682 } else static if(is(typeof(w) == DropDownSelection)) { 4683 // special case for this to kinda support enums and such. could be better though 4684 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4685 } else { 4686 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4687 } 4688 +/ 4689 } 4690 } 4691 4692 /++ 4693 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4694 4695 History: 4696 Added May 28, 2021 4697 +/ 4698 void notifyDataUpdated() { 4699 foreach(updater; updaters) 4700 updater(); 4701 4702 this.emit!(ChangeEvent!void)(delegate{}); 4703 } 4704 4705 private Widget[string] memberWidgets; 4706 private void delegate()[] updaters; 4707 4708 mixin Emits!(ChangeEvent!void); 4709 } 4710 4711 private int saturatedSum(int[] values...) { 4712 int sum; 4713 foreach(value; values) { 4714 if(value == int.max) 4715 return int.max; 4716 sum += value; 4717 } 4718 return sum; 4719 } 4720 4721 void genericSetValue(T, W)(T* where, W what) { 4722 version(D_OpenD) { 4723 static if(is(T == int[])) { 4724 // pragma(msg, "FIXME"); 4725 } else 4726 static if(is(W : T)) { 4727 *where = what; 4728 } else 4729 { 4730 import arsd.conv; 4731 *where = to!T(what); 4732 } 4733 } else { 4734 // slow, less feature fallback branch cuz i hate dub 4735 import std.conv; 4736 *where = to!T(what); 4737 } 4738 } 4739 4740 /++ 4741 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4742 4743 The `updateWidgetFromData` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4744 4745 Note that this creates the widget but does not attach any event handlers to it. You might set a change event to call this. 4746 +/ 4747 private static auto widgetFor(alias tt, P)(P* valptr, Widget parent, out void delegate() updateWidgetFromData, out void delegate() updateDataFromWidget) { 4748 4749 string displayName = __traits(identifier, tt).beautify; 4750 4751 static if(controlledByCount!tt == 1) { 4752 foreach(i, attr; __traits(getAttributes, tt)) { 4753 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4754 auto w = attr.construct(parent); 4755 static if(__traits(compiles, w.setPosition(*valptr))) { 4756 updateWidgetFromData = () { w.setPosition(*valptr); }; 4757 updateDataFromWidget = () { *valptr = w.position; }; 4758 } 4759 else static if(__traits(compiles, w.setValue(*valptr))) { 4760 updateWidgetFromData = () { w.setValue(*valptr); }; 4761 updateDataFromWidget = () { *valptr = w.value; }; 4762 } 4763 4764 if(updateWidgetFromData) 4765 updateWidgetFromData(); 4766 return w; 4767 } 4768 } 4769 } else static if(controlledByCount!tt == 0) { 4770 4771 version(D_OpenD) 4772 import arsd.conv; 4773 else 4774 import std.conv; 4775 4776 static if(choicesCount!tt == 1) { 4777 auto choices = ChoicesFor!tt; 4778 4779 static if(is(typeof(tt) == E[], E)) { 4780 // can select multiple... 4781 auto list = new Fieldset(displayName, parent); 4782 4783 Checkbox[] boxes; 4784 4785 foreach(option; choices.options()) { 4786 boxes ~= new Checkbox(option, list); 4787 } 4788 4789 updateWidgetFromData = () { 4790 foreach(box; boxes) { 4791 box.isChecked = box.label == *valptr; 4792 } 4793 }; 4794 4795 updateDataFromWidget = () { 4796 (*valptr) = []; 4797 foreach(idx, box; boxes) { 4798 // FIXME: what if it is not an int[]? 4799 if(box.isChecked) 4800 (*valptr) ~= cast(int) idx; 4801 } 4802 }; 4803 4804 return list; 4805 } else { 4806 auto dds = new DropDownSelection(parent); 4807 4808 // FIXME: label 4809 4810 foreach(option; choices.options()) { 4811 // FIXME: options need not be strings 4812 dds.addOption(option); 4813 } 4814 4815 // FIXME: value need not be ints... 4816 updateWidgetFromData = () { 4817 dds.setSelection(*valptr); 4818 }; 4819 updateDataFromWidget = () { 4820 if(dds.getSelection != -1) 4821 *valptr = cast(P) dds.getSelection; 4822 }; 4823 updateWidgetFromData(); 4824 4825 return dds; 4826 } 4827 4828 /+ // FIXME consider these things: 4829 bool allowCustom = false; 4830 /// only relevant if attached to an array 4831 bool allowReordering = true; 4832 /// ditto 4833 bool allowDuplicates = true; 4834 /// makes no sense on a set 4835 bool requireAll = false; 4836 +/ 4837 } else static if(choicesCount!tt == 0) { 4838 4839 static if(is(typeof(tt) == enum)) { 4840 // FIXME: label 4841 auto dds = new DropDownSelection(parent); 4842 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4843 dds.addOption(option); 4844 } 4845 updateWidgetFromData = () { 4846 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4847 if(__traits(getMember, typeof(tt), option) == *valptr) 4848 dds.setSelection(cast(int) idx); 4849 } 4850 }; 4851 updateDataFromWidget = () { 4852 if(dds.getSelection != -1) 4853 *valptr = cast(P) dds.getSelection; 4854 }; 4855 updateWidgetFromData(); 4856 return dds; 4857 } else static if(is(typeof(tt) == bool)) { 4858 auto box = new Checkbox(displayName, parent); 4859 updateWidgetFromData = () { box.isChecked = *valptr; }; 4860 updateDataFromWidget = () { *valptr = box.isChecked; }; 4861 updateWidgetFromData(); 4862 return box; 4863 } else static if(is(typeof(tt) : const long)) { 4864 auto le = new LabeledLineEdit(displayName, parent); 4865 updateWidgetFromData = () { le.content = toInternal!string(*valptr); }; 4866 updateDataFromWidget = () { *valptr = to!P(le.content); }; 4867 updateWidgetFromData(); 4868 return le; 4869 } else static if(is(typeof(tt) : const double)) { 4870 auto le = new LabeledLineEdit(displayName, parent); 4871 version(D_OpenD) 4872 import arsd.conv; 4873 else 4874 import std.conv; 4875 updateWidgetFromData = () { le.content = to!string(*valptr); }; 4876 updateDataFromWidget = () { *valptr = to!P(le.content); }; 4877 updateWidgetFromData(); 4878 return le; 4879 } else static if(is(typeof(tt) : const string)) { 4880 auto le = new LabeledLineEdit(displayName, parent); 4881 updateWidgetFromData = () { le.content = *valptr; }; 4882 updateDataFromWidget = () { *valptr = to!P(le.content); }; 4883 updateWidgetFromData(); 4884 return le; 4885 } else static if(is(typeof(tt) == E[], E)) { 4886 auto w = new ArrayEditingWidget!E(parent); 4887 // FIXME updateWidgetFromData 4888 return w; 4889 } else static if(is(typeof(tt) == function)) { 4890 auto w = new Button(displayName, parent); 4891 return w; 4892 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4893 // FIXME: updaters 4894 return parent.addDataControllerWidget(tt); 4895 } else static assert(0, typeof(tt).stringof); 4896 } else static assert(0, "multiple choices not supported"); 4897 } else static assert(0, "multiple controllers not yet supported"); 4898 } 4899 4900 class ArrayEditingWidget(T) : ArrayEditingWidgetBase { 4901 this(Widget parent) { 4902 super(parent); 4903 } 4904 } 4905 4906 class ArrayEditingWidgetBase : Widget { 4907 this(Widget parent) { 4908 super(parent); 4909 4910 // FIXME: a trash can to move items into to delete them? 4911 static class MyListViewItem : GenericListViewItem { 4912 this(Widget parent) { 4913 super(parent); 4914 4915 /+ 4916 drag handle 4917 left click lets you move the whole selection. if the current element is not selected, it changes the selection to it. 4918 right click here gives you the movement controls too 4919 index/key view zone 4920 left click here selects/unselects 4921 element view/edit zone 4922 delete button 4923 +/ 4924 4925 // FIXME: make sure the index is viewable 4926 4927 auto hl = new HorizontalLayout(this); 4928 4929 button = new CommandButton("d", hl); 4930 4931 label = new TextLabel("unloaded", TextAlignment.Left, hl); 4932 // if member editable, have edit view... get from the subclass. 4933 4934 // or a "..." menu? 4935 button = new CommandButton("Up", hl); // shift+click is move to top 4936 button = new CommandButton("Down", hl); // shift+click is move to bottom 4937 button = new CommandButton("Move to", hl); // move before, after, or swap 4938 button = new CommandButton("Delete", hl); 4939 4940 button.addEventListener("triggered", delegate(){ 4941 //messageBox(text("clicked ", currentIndexLoaded())); 4942 }); 4943 } 4944 override void showItem(int idx) { 4945 label.label = "Item ";// ~ to!string(idx); 4946 } 4947 4948 TextLabel label; 4949 Button button; 4950 } 4951 4952 auto outer_this = this; 4953 4954 // FIXME: make sure item count is easy to see 4955 4956 glvw = new class GenericListViewWidget { 4957 this() { 4958 super(outer_this); 4959 } 4960 override GenericListViewItem itemFactory(Widget parent) { 4961 return new MyListViewItem(parent); 4962 } 4963 override Size itemSize() { 4964 return Size(0, scaleWithDpi(80)); 4965 } 4966 4967 override Menu contextMenu(int x, int y) { 4968 return createContextMenuFromAnnotatedCode(this); 4969 } 4970 4971 @context_menu { 4972 void Select_All() { 4973 4974 } 4975 4976 void Undo() { 4977 4978 } 4979 4980 void Redo() { 4981 4982 } 4983 4984 void Cut() { 4985 4986 } 4987 4988 void Copy() { 4989 4990 } 4991 4992 void Paste() { 4993 4994 } 4995 4996 void Delete() { 4997 4998 } 4999 5000 void Find() { 5001 5002 } 5003 } 5004 }; 5005 5006 glvw.setItemCount(400); 5007 5008 auto hl = new HorizontalLayout(this); 5009 add = new FreeEntrySelection(hl); 5010 addButton = new Button("Add", hl); 5011 } 5012 5013 GenericListViewWidget glvw; 5014 ComboboxBase add; 5015 Button addButton; 5016 /+ 5017 Controls: 5018 clear (select all / delete) 5019 reset (confirmation blocked button, maybe only on the whole form? or hit undo so many times to get back there) 5020 add item 5021 palette of options to add to the array (add prolly a combo box) 5022 rearrange - move up/down, drag and drop a selection? right click can always do, left click only drags when on a selection handle. 5023 edit/input/view items (GLVW? or it could be a table view in a way.) 5024 undo/redo 5025 select whole elements (even if a struct) 5026 cut/copy/paste elements 5027 5028 could have an element picker, a details pane, and an add bare? 5029 5030 5031 put a handle on the elements for left click dragging. allow right click drag anywhere but pretty big wiggle until it enables. 5032 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. 5033 the handle should let dragging w/o changing the selection, or if part of the selection, drag the whole selection i think. 5034 make it textured and use the grabby hand mouse cursor. 5035 +/ 5036 } 5037 5038 /++ 5039 A button that pops up a menu on click for working on a particular item or selection. 5040 5041 History: 5042 Added March 23, 2025 5043 +/ 5044 class MenuPopupButton : Button { 5045 /++ 5046 You might consider using [createContextMenuFromAnnotatedCode] to populate the `menu` argument. 5047 5048 You also may want to set the [prepare] delegate after construction. 5049 +/ 5050 this(Menu menu, Widget parent) { 5051 assert(menu !is null); 5052 5053 this.menu = menu; 5054 super("...", parent); 5055 } 5056 5057 private Menu menu; 5058 /++ 5059 If set, this delegate is called before popping up the window. This gives you a chance 5060 to prepare your dynamic data structures for the element(s) selected. 5061 5062 For example, if your `MenuPopupButton` is attached to a [GenericListViewItem], you can call 5063 [GenericListViewItem.currentIndexLoaded] in here and set it to a variable in the object you 5064 called [createContextMenuFromAnnotatedCode] to apply the operation to the right object. 5065 5066 (The api could probably be simpler...) 5067 +/ 5068 void delegate() prepare; 5069 5070 override void defaultEventHandler_triggered(scope Event e) { 5071 if(prepare) 5072 prepare(); 5073 showContextMenu(this.x, this.y + this.height, -2, -2, menu); 5074 } 5075 5076 override int maxHeight() { 5077 return defaultLineHeight; 5078 } 5079 5080 override int maxWidth() { 5081 return defaultLineHeight; 5082 } 5083 } 5084 5085 /++ 5086 A button that pops up an information box, similar to a tooltip, but explicitly triggered. 5087 5088 FIXME: i want to be able to easily embed these in other things too. 5089 +/ 5090 class TipPopupButton : Button { 5091 /++ 5092 +/ 5093 this(Widget delegate(Widget p) factory, Widget parent) { 5094 this.factory = factory; 5095 super("?", parent); 5096 } 5097 /// ditto 5098 this(string tip, Widget parent) { 5099 this((parent) { 5100 auto td = new TextDisplayTooltip(tip, parent); 5101 return td; 5102 }, parent); 5103 } 5104 5105 private Widget delegate(Widget p) factory; 5106 5107 override void defaultEventHandler_triggered(scope Event e) { 5108 auto window = new TooltipWindow(factory, this); 5109 window.popup(this); 5110 } 5111 5112 private static class TextDisplayTooltip : TextDisplay { 5113 this(string txt, Widget parent) { 5114 super(txt, parent); 5115 } 5116 5117 // override int minHeight() { return defaultLineHeight; } 5118 // override int flexBasisHeight() { return defaultLineHeight; } 5119 5120 static class Style : TextDisplay.Style { 5121 override WidgetBackground background() { 5122 return WidgetBackground(Color.yellow); 5123 } 5124 5125 override FrameStyle borderStyle() { 5126 return FrameStyle.solid; 5127 } 5128 5129 override Color borderColor() { 5130 return Color.black; 5131 } 5132 } 5133 5134 mixin OverrideStyle!Style; 5135 } 5136 } 5137 5138 /++ 5139 History: 5140 Added March 23, 2025 5141 +/ 5142 class TooltipWindow : Window { 5143 5144 private Widget previouslyFocusedWidget; 5145 private Widget* previouslyFocusedWidgetBelongsIn; 5146 5147 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 5148 if(offsetY == int.min) 5149 offsetY = 0; 5150 5151 int w = child.flexBasisWidth(); 5152 int h = child.flexBasisHeight() + this.paddingTop + this.paddingBottom + /* horiz scroll bar - FIXME */ 16 + 2 /* for border */; 5153 5154 auto coord = parent.globalCoordinates(); 5155 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 5156 5157 this.width = w; 5158 this.height = h; 5159 5160 this.recomputeChildLayout(); 5161 5162 static if(UsingSimpledisplayX11) 5163 XSync(XDisplayConnection.get, 0); 5164 5165 dropDown.visibilityChanged = (bool visible) { 5166 if(visible) { 5167 this.redraw(); 5168 //dropDown.grabInput(); 5169 captureMouse(this); 5170 5171 if(previouslyFocusedWidget is null) 5172 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 5173 parent.parentWindow.focusedWidget = this; 5174 } else { 5175 releaseMouseCapture(); 5176 //dropDown.releaseInputGrab(); 5177 5178 parent.parentWindow.focusedWidget = previouslyFocusedWidget; 5179 5180 static if(UsingSimpledisplayX11) 5181 flushGui(); 5182 } 5183 }; 5184 5185 dropDown.show(); 5186 5187 clickListener = this.addEventListener((scope ClickEvent ev) { 5188 if(ev.target is this) { 5189 unpopup(); 5190 } 5191 }, true /* again for asap action */); 5192 } 5193 5194 private EventListener clickListener; 5195 5196 void unpopup() { 5197 mouseLastOver = mouseLastDownOn = null; 5198 dropDown.hide(); 5199 clickListener.disconnect(); 5200 } 5201 5202 override void defaultEventHandler_char(CharEvent ce) { 5203 if(ce.character == '\033') 5204 unpopup(); 5205 } 5206 5207 private SimpleWindow dropDown; 5208 private Widget child; 5209 5210 /// 5211 this(Widget delegate(Widget p) factory, Widget parent) { 5212 assert(parent); 5213 assert(parent.parentWindow); 5214 assert(parent.parentWindow.win); 5215 dropDown = new SimpleWindow( 5216 250, 40, 5217 null, OpenGlOptions.no, Resizability.fixedSize, 5218 WindowTypes.tooltip, 5219 WindowFlags.dontAutoShow, 5220 parent ? parent.parentWindow.win : null 5221 ); 5222 5223 super(dropDown); 5224 5225 child = factory(this); 5226 } 5227 } 5228 5229 private template controlledByCount(alias tt) { 5230 static int helper() { 5231 int count; 5232 foreach(i, attr; __traits(getAttributes, tt)) 5233 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 5234 count++; 5235 return count; 5236 } 5237 5238 enum controlledByCount = helper; 5239 } 5240 5241 private template choicesCount(alias tt) { 5242 static int helper() { 5243 int count; 5244 foreach(i, attr; __traits(getAttributes, tt)) 5245 static if(is(typeof(attr) == Choices!T, T)) 5246 count++; 5247 return count; 5248 } 5249 5250 enum choicesCount = helper; 5251 } 5252 5253 private template ChoicesFor(alias tt) { 5254 static int helper() { 5255 int count; 5256 foreach(i, attr; __traits(getAttributes, tt)) 5257 static if(is(typeof(attr) == Choices!T, T)) 5258 return i; 5259 return -1; 5260 } 5261 5262 static immutable ChoicesFor = __traits(getAttributes, tt)[helper()]; // FIXME: change static to enum and get illegal instruction from dmd backend 5263 } 5264 5265 /++ 5266 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 5267 5268 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 5269 5270 History: 5271 The `redrawOnChange` parameter was added on May 28, 2021. 5272 +/ 5273 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 5274 auto dcw = new DataControllerWidget!T(t, parent); 5275 initializeDataControllerWidget(dcw, redrawOnChange); 5276 return dcw; 5277 } 5278 5279 /// ditto 5280 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 5281 auto dcw = new DataControllerWidget!T(t, parent); 5282 initializeDataControllerWidget(dcw, redrawOnChange); 5283 return dcw; 5284 } 5285 5286 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 5287 if(redrawOnChange !is null) 5288 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 5289 } 5290 5291 /++ 5292 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. 5293 5294 History: 5295 Finalized on June 3, 2021 for the dub v10.0 release 5296 +/ 5297 struct StyleInformation { 5298 private Widget w; 5299 private BaseVisualTheme visualTheme; 5300 5301 private this(Widget w) { 5302 this.w = w; 5303 this.visualTheme = WidgetPainter.visualTheme; 5304 } 5305 5306 /++ 5307 Forwards to [Widget.Style] 5308 5309 Bugs: 5310 It is supposed to fall back to the [VisualTheme] if 5311 the style doesn't override the default, but that is 5312 not generally implemented. Many of them may end up 5313 being explicit overloads instead of the generic 5314 opDispatch fallback, like [font] is now. 5315 +/ 5316 public @property opDispatch(string name)() { 5317 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 5318 w.useStyleProperties((scope Widget.Style props) { 5319 //visualTheme.useStyleProperties(w, (props) { 5320 prop = __traits(getMember, props, name); 5321 }); 5322 return prop; 5323 } 5324 5325 /++ 5326 Returns the cached font object associated with the widget, 5327 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 5328 5329 History: 5330 Prior to March 21, 2022 (dub v10.7), `font` went through 5331 [opDispatch], which did not use the cache. You can now call it 5332 repeatedly without guilt. 5333 +/ 5334 public @property OperatingSystemFont font() { 5335 OperatingSystemFont prop; 5336 w.useStyleProperties((scope Widget.Style props) { 5337 prop = props.fontCached; 5338 }); 5339 if(prop is null) { 5340 prop = visualTheme.defaultFontCached(w.currentDpi); 5341 } 5342 return prop; 5343 } 5344 5345 @property { 5346 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 5347 /** */ int paddingLeft() { return w.paddingLeft(); } 5348 /** */ int paddingRight() { return w.paddingRight(); } 5349 /** */ int paddingTop() { return w.paddingTop(); } 5350 /** */ int paddingBottom() { return w.paddingBottom(); } 5351 5352 /** */ int marginLeft() { return w.marginLeft(); } 5353 /** */ int marginRight() { return w.marginRight(); } 5354 /** */ int marginTop() { return w.marginTop(); } 5355 /** */ int marginBottom() { return w.marginBottom(); } 5356 5357 /** */ int maxHeight() { return w.maxHeight(); } 5358 /** */ int minHeight() { return w.minHeight(); } 5359 5360 /** */ int maxWidth() { return w.maxWidth(); } 5361 /** */ int minWidth() { return w.minWidth(); } 5362 5363 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 5364 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 5365 5366 /** */ int heightStretchiness() { return w.heightStretchiness(); } 5367 /** */ int widthStretchiness() { return w.widthStretchiness(); } 5368 5369 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 5370 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 5371 5372 // Global helpers some of these are unstable. 5373 static: 5374 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 5375 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 5376 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 5377 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 5378 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 5379 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5380 5381 /** */ Color activeTabColor() { return lightAccentColor; } 5382 /** */ Color buttonColor() { return windowBackgroundColor; } 5383 /** */ Color depressedButtonColor() { return darkAccentColor; } 5384 /** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; } 5385 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 5386 auto c = WidgetPainter.visualTheme.selectionColor(); 5387 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 5388 } 5389 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5390 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5391 } 5392 5393 5394 5395 /+ 5396 5397 private static auto extractStyleProperty(string name)(Widget w) { 5398 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 5399 w.useStyleProperties((props) { 5400 prop = __traits(getMember, props, name); 5401 }); 5402 return prop; 5403 } 5404 5405 // FIXME: clear this upon a X server disconnect 5406 private static OperatingSystemFont[string] fontCache; 5407 5408 T getProperty(T)(string name, lazy T default_) { 5409 if(visualTheme !is null) { 5410 auto str = visualTheme.getPropertyString(w, name); 5411 if(str is null) 5412 return default_; 5413 static if(is(T == Color)) 5414 return Color.fromString(str); 5415 else static if(is(T == Measurement)) 5416 return Measurement(cast(int) toInternal!int(str)); 5417 else static if(is(T == WidgetBackground)) 5418 return WidgetBackground.fromString(str); 5419 else static if(is(T == OperatingSystemFont)) { 5420 if(auto f = str in fontCache) 5421 return *f; 5422 else 5423 return fontCache[str] = new OperatingSystemFont(str); 5424 } else static if(is(T == FrameStyle)) { 5425 switch(str) { 5426 default: 5427 return FrameStyle.none; 5428 foreach(style; __traits(allMembers, FrameStyle)) 5429 case style: 5430 return __traits(getMember, FrameStyle, style); 5431 } 5432 } else static assert(0); 5433 } else 5434 return default_; 5435 } 5436 5437 static struct Measurement { 5438 int value; 5439 alias value this; 5440 } 5441 5442 @property: 5443 5444 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 5445 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 5446 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 5447 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 5448 5449 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 5450 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 5451 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 5452 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 5453 5454 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 5455 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 5456 5457 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 5458 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 5459 5460 5461 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 5462 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 5463 5464 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 5465 5466 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 5467 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 5468 5469 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 5470 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 5471 5472 5473 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 5474 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 5475 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 5476 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 5477 5478 Color activeTabColor() { return lightAccentColor; } 5479 Color buttonColor() { return windowBackgroundColor; } 5480 Color depressedButtonColor() { return darkAccentColor; } 5481 Color hoveringColor() { return Color(228, 228, 228); } 5482 Color activeListXorColor() { 5483 auto c = WidgetPainter.visualTheme.selectionColor(); 5484 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 5485 } 5486 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 5487 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 5488 +/ 5489 } 5490 5491 5492 5493 // pragma(msg, __traits(classInstanceSize, Widget)); 5494 5495 /*private*/ template EventString(E) { 5496 static if(is(typeof(E.EventString))) 5497 enum EventString = E.EventString; 5498 else 5499 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 5500 } 5501 5502 /*private*/ template EventStringIdentifier(E) { 5503 string helper() { 5504 auto es = EventString!E; 5505 char[] id = new char[](es.length * 2); 5506 size_t idx; 5507 foreach(char ch; es) { 5508 id[idx++] = cast(char)('a' + (ch >> 4)); 5509 id[idx++] = cast(char)('a' + (ch & 0x0f)); 5510 } 5511 return cast(string) id; 5512 } 5513 5514 enum EventStringIdentifier = helper(); 5515 } 5516 5517 5518 template classStaticallyEmits(This, EventType) { 5519 static if(is(This Base == super)) 5520 static if(is(Base : Widget)) 5521 enum baseEmits = classStaticallyEmits!(Base, EventType); 5522 else 5523 enum baseEmits = false; 5524 else 5525 enum baseEmits = false; 5526 5527 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 5528 5529 enum classStaticallyEmits = thisEmits || baseEmits; 5530 } 5531 5532 /++ 5533 A helper to make widgets out of other native windows. 5534 5535 History: 5536 Factored out of OpenGlWidget on November 5, 2021 5537 +/ 5538 class NestedChildWindowWidget : Widget { 5539 SimpleWindow win; 5540 5541 /++ 5542 Used on X to send focus to the appropriate child window when requested by the window manager. 5543 5544 Normally returns its own nested window. Can also return another child or null to revert to the parent 5545 if you override it in a child class. 5546 5547 History: 5548 Added April 2, 2022 (dub v10.8) 5549 +/ 5550 SimpleWindow focusableWindow() { 5551 return win; 5552 } 5553 5554 /// 5555 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5556 this(SimpleWindow win, Widget parent) { 5557 this.parentWindow = parent.parentWindow; 5558 this.win = win; 5559 5560 super(parent); 5561 windowsetup(win); 5562 } 5563 5564 static protected SimpleWindow getParentWindow(Widget parent) { 5565 assert(parent !is null); 5566 SimpleWindow pwin = parent.parentWindow.win; 5567 5568 version(win32_widgets) { 5569 HWND phwnd; 5570 auto wtf = parent; 5571 while(wtf) { 5572 if(wtf.hwnd) { 5573 phwnd = wtf.hwnd; 5574 break; 5575 } 5576 wtf = wtf.parent; 5577 } 5578 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 5579 if(phwnd) 5580 pwin = new SimpleWindow(phwnd); 5581 } 5582 5583 return pwin; 5584 } 5585 5586 /++ 5587 Called upon the nested window being destroyed. 5588 Remember the window has already been destroyed at 5589 this point, so don't use the native handle for anything. 5590 5591 History: 5592 Added April 3, 2022 (dub v10.8) 5593 +/ 5594 protected void dispose() { 5595 5596 } 5597 5598 protected void windowsetup(SimpleWindow w) { 5599 /* 5600 win.onFocusChange = (bool getting) { 5601 if(getting) 5602 this.focus(); 5603 }; 5604 */ 5605 5606 /+ 5607 win.onFocusChange = (bool getting) { 5608 if(getting) { 5609 this.parentWindow.focusedWidget = this; 5610 this.emit!FocusEvent(); 5611 this.emit!FocusInEvent(); 5612 } else { 5613 this.emit!BlurEvent(); 5614 this.emit!FocusOutEvent(); 5615 } 5616 }; 5617 +/ 5618 5619 win.onDestroyed = () { 5620 this.dispose(); 5621 }; 5622 5623 version(win32_widgets) { 5624 Widget.nativeMapping[win.hwnd] = this; 5625 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 5626 } else { 5627 win.setEventHandlers( 5628 (MouseEvent e) { 5629 Widget p = this; 5630 while(p ! is parentWindow) { 5631 e.x += p.x; 5632 e.y += p.y; 5633 p = p.parent; 5634 } 5635 parentWindow.dispatchMouseEvent(e); 5636 }, 5637 (KeyEvent e) { 5638 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 5639 parentWindow.dispatchKeyEvent(e); 5640 }, 5641 (dchar e) { 5642 parentWindow.dispatchCharEvent(e); 5643 }, 5644 ); 5645 } 5646 5647 } 5648 5649 override bool showOrHideIfNativeWindow(bool shouldShow) { 5650 auto cur = hidden; 5651 win.hidden = !shouldShow; 5652 if(cur != shouldShow && shouldShow) 5653 redraw(); 5654 return true; 5655 } 5656 5657 /// OpenGL widgets cannot have child widgets. Do not call this. 5658 /* @disable */ final override void addChild(Widget, int) { 5659 throw new Error("cannot add children to OpenGL widgets"); 5660 } 5661 5662 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 5663 /// Keep in mind that events like mouse coordinates are still relative to your size. 5664 override void registerMovement() { 5665 // writefln("%d %d %d %d", x,y,width,height); 5666 version(win32_widgets) 5667 auto pos = getChildPositionRelativeToParentHwnd(this); 5668 else 5669 auto pos = getChildPositionRelativeToParentOrigin(this); 5670 win.moveResize(pos[0], pos[1], width, height); 5671 5672 registerMovementAdditionalWork(); 5673 sendResizeEvent(); 5674 } 5675 5676 abstract void registerMovementAdditionalWork(); 5677 } 5678 5679 /++ 5680 Nests an opengl capable window inside this window as a widget. 5681 5682 You may also just want to create an additional [SimpleWindow] with 5683 [OpenGlOptions.yes] yourself. 5684 5685 An OpenGL widget cannot have child widgets. It will throw if you try. 5686 +/ 5687 static if(OpenGlEnabled) 5688 class OpenGlWidget : NestedChildWindowWidget { 5689 5690 override void registerMovementAdditionalWork() { 5691 win.setAsCurrentOpenGlContext(); 5692 } 5693 5694 /// 5695 this(Widget parent) { 5696 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5697 super(win, parent); 5698 } 5699 5700 override void paint(WidgetPainter painter) { 5701 win.setAsCurrentOpenGlContext(); 5702 glViewport(0, 0, this.width, this.height); 5703 win.redrawOpenGlSceneNow(); 5704 } 5705 5706 void redrawOpenGlScene(void delegate() dg) { 5707 win.redrawOpenGlScene = dg; 5708 } 5709 } 5710 5711 /++ 5712 This demo shows how to draw text in an opengl scene. 5713 +/ 5714 unittest { 5715 import arsd.minigui; 5716 import arsd.ttf; 5717 5718 void main() { 5719 auto window = new Window(); 5720 5721 auto widget = new OpenGlWidget(window); 5722 5723 // old means non-shader code so compatible with glBegin etc. 5724 // tbh I haven't implemented new one in font yet... 5725 // anyway, declaring here, will construct soon. 5726 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 5727 5728 // this is a little bit awkward, calling some methods through 5729 // the underlying SimpleWindow `win` method, and you can't do this 5730 // on a nanovega widget due to conflicts so I should probably fix 5731 // the api to be a bit easier. But here it will work. 5732 // 5733 // Alternatively, you could load the font on the first draw, inside 5734 // the redrawOpenGlScene, and keep a flag so you don't do it every 5735 // time. That'd be a bit easier since the lib sets up the context 5736 // by then guaranteed. 5737 // 5738 // But still, I wanna show this. 5739 widget.win.visibleForTheFirstTime = delegate { 5740 // must set the opengl context 5741 widget.win.setAsCurrentOpenGlContext(); 5742 5743 // if you were doing a OpenGL 3+ shader, this 5744 // gets especially important to do in order. With 5745 // old-style opengl, I think you can even do it 5746 // in main(), but meh, let's show it more correctly. 5747 5748 // Anyway, now it is time to load the font from the 5749 // OS (you can alternatively load one from a .ttf file 5750 // you bundle with the application), then load the 5751 // font into texture for drawing. 5752 5753 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 5754 5755 assert(!osfont.isNull()); // make sure it actually loaded 5756 5757 // using typeof to avoid repeating the long name lol 5758 glfont = new typeof(glfont)( 5759 // get the raw data from the font for loading in here 5760 // since it doesn't use the OS function to draw the 5761 // text, we gotta treat it more as a file than as 5762 // a drawing api. 5763 osfont.getTtfBytes(), 5764 18, // need to respecify size since opengl world is different coordinate system 5765 5766 // these last two numbers are why it is called 5767 // "Limited" font. It only loads the characters 5768 // in the given range, since the texture atlas 5769 // it references is all a big image generated ahead 5770 // of time. You could maybe do the whole thing but 5771 // idk how much memory that is. 5772 // 5773 // But here, 0-128 represents the ASCII range, so 5774 // good enough for most English things, numeric labels, 5775 // etc. 5776 0, 5777 128 5778 ); 5779 }; 5780 5781 widget.redrawOpenGlScene = () { 5782 // now we can use the glfont's drawString function 5783 5784 // first some opengl setup. You can do this in one place 5785 // on window first visible too in many cases, just showing 5786 // here cuz it is easier for me. 5787 5788 // gonna need some alpha blending or it just looks awful 5789 glEnable(GL_BLEND); 5790 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5791 glClearColor(0,0,0,0); 5792 glDepthFunc(GL_LEQUAL); 5793 5794 // Also need to enable 2d textures, since it draws the 5795 // font characters as images baked in 5796 glMatrixMode(GL_MODELVIEW); 5797 glLoadIdentity(); 5798 glDisable(GL_DEPTH_TEST); 5799 glEnable(GL_TEXTURE_2D); 5800 5801 // the orthographic matrix is best for 2d things like text 5802 // so let's set that up. This matrix makes the coordinates 5803 // in the opengl scene be one-to-one with the actual pixels 5804 // on screen. (Not necessarily best, you may wish to scale 5805 // things, but it does help keep fonts looking normal.) 5806 glMatrixMode(GL_PROJECTION); 5807 glLoadIdentity(); 5808 glOrtho(0, widget.width, widget.height, 0, 0, 1); 5809 5810 // you can do other glScale, glRotate, glTranslate, etc 5811 // to the matrix here of course if you want. 5812 5813 // note the x,y coordinates here are for the text baseline 5814 // NOT the upper-left corner. The baseline is like the line 5815 // in the notebook you write on. Most the letters are actually 5816 // above it, but some, like p and q, dip a bit below it. 5817 // 5818 // So if you're used to the upper left coordinate like the 5819 // rest of simpledisplay/minigui usually do, do the 5820 // y + glfont.ascent to bring it down a little. So this 5821 // example puts the string in the upper left of the window. 5822 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 5823 5824 // re color btw: the function sets a solid color internally, 5825 // but you actually COULD do your own thing for rainbow effects 5826 // and the sort if you wanted too, by pulling its guts out. 5827 // Just view its source for an idea of how it actually draws: 5828 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 5829 5830 // it gets a bit complicated with the character positioning, 5831 // but the opengl parts are fairly simple: bind a texture, 5832 // set the color, draw a quad for each letter. 5833 5834 5835 // the last optional argument there btw is a bounding box 5836 // it will/ use to word wrap and return an object you can 5837 // use to implement scrolling or pagination; it tells how 5838 // much of the string didn't fit in the box. But for simple 5839 // labels we can just ignore that. 5840 5841 5842 // I'd suggest drawing text as the last step, after you 5843 // do your other drawing. You might use the push/pop matrix 5844 // stuff to keep your place. You, in theory, should be able 5845 // to do text in a 3d space but I've never actually tried 5846 // that.... 5847 }; 5848 5849 window.loop(); 5850 } 5851 } 5852 5853 version(custom_widgets) 5854 private class TextListViewWidget : GenericListViewWidget { 5855 static class TextListViewItem : GenericListViewItem { 5856 ListWidget controller; 5857 this(ListWidget controller, Widget parent) { 5858 this.controller = controller; 5859 this.tabStop = false; 5860 super(parent); 5861 } 5862 5863 ListWidget.Option* showing; 5864 5865 override void showItem(int idx) { 5866 showing = idx < controller.options.length ? &controller.options[idx] : null; 5867 redraw(); // is this necessary? the generic thing might call it... 5868 } 5869 5870 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 5871 if(showing is null) 5872 return bounds; 5873 painter.drawText(bounds.upperLeft, showing.label); 5874 return bounds; 5875 } 5876 5877 static class Style : Widget.Style { 5878 override WidgetBackground background() { 5879 // FIXME: change it if it is focused or not 5880 // 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 5881 auto tlvi = cast(TextListViewItem) widget; 5882 if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected) 5883 return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode 5884 return super.background(); 5885 } 5886 5887 override Color foregroundColor() { 5888 auto tlvi = cast(TextListViewItem) widget; 5889 return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor(); 5890 } 5891 5892 override FrameStyle outlineStyle() { 5893 // FIXME: change it if it is focused or not 5894 auto tlvi = cast(TextListViewItem) widget; 5895 return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle(); 5896 } 5897 } 5898 mixin OverrideStyle!Style; 5899 5900 mixin Padding!q{2}; 5901 5902 override void defaultEventHandler_click(ClickEvent event) { 5903 if(event.button == MouseButton.left) { 5904 controller.setSelection(currentIndexLoaded()); 5905 controller.focusOn = currentIndexLoaded(); 5906 } 5907 } 5908 5909 } 5910 5911 ListWidget controller; 5912 5913 this(ListWidget parent) { 5914 this.controller = parent; 5915 this.tabStop = false; // this is only used as a child of the ListWidget 5916 super(parent); 5917 5918 smw.movementPerButtonClick(1, itemSize().height); 5919 } 5920 5921 override Size itemSize() { 5922 return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */)); 5923 } 5924 5925 override GenericListViewItem itemFactory(Widget parent) { 5926 return new TextListViewItem(controller, parent); 5927 } 5928 5929 static class Style : Widget.Style { 5930 override FrameStyle borderStyle() { 5931 return FrameStyle.sunk; 5932 } 5933 5934 override WidgetBackground background() { 5935 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 5936 } 5937 } 5938 mixin OverrideStyle!Style; 5939 } 5940 5941 /++ 5942 A list widget contains a list of strings that the user can examine and select. 5943 5944 5945 In the future, items in the list may be possible to be more than just strings. 5946 5947 See_Also: 5948 [TableView] 5949 +/ 5950 class ListWidget : Widget { 5951 /// 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. 5952 mixin Emits!(ChangeEvent!void); 5953 5954 version(custom_widgets) 5955 TextListViewWidget glvw; 5956 5957 static struct Option { 5958 string label; 5959 bool selected; 5960 void* tag; 5961 } 5962 private Option[] options; 5963 5964 /++ 5965 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 5966 +/ 5967 void setSelection(int y) { 5968 if(!multiSelect) 5969 foreach(ref opt; options) 5970 opt.selected = false; 5971 if(y >= 0 && y < options.length) 5972 options[y].selected = !options[y].selected; 5973 5974 version(custom_widgets) 5975 focusOn = y; 5976 5977 this.emit!(ChangeEvent!void)(delegate {}); 5978 5979 version(custom_widgets) 5980 redraw(); 5981 } 5982 5983 /++ 5984 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 5985 Returns -1 if nothing is selected. 5986 +/ 5987 int getSelection() 5988 { 5989 foreach(i, opt; options) { 5990 if (opt.selected) 5991 return cast(int) i; 5992 } 5993 return -1; 5994 } 5995 5996 version(custom_widgets) 5997 private int focusOn; 5998 5999 this(Widget parent) { 6000 super(parent); 6001 6002 version(custom_widgets) 6003 glvw = new TextListViewWidget(this); 6004 6005 version(win32_widgets) 6006 createWin32Window(this, WC_LISTBOX, "", 6007 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 6008 } 6009 6010 version(win32_widgets) 6011 override void handleWmCommand(ushort code, ushort id) { 6012 switch(code) { 6013 case LBN_SELCHANGE: 6014 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 6015 setSelection(cast(int) sel); 6016 break; 6017 default: 6018 } 6019 } 6020 6021 6022 void addOption(string text, void* tag = null) { 6023 options ~= Option(text, false, tag); 6024 version(win32_widgets) { 6025 WCharzBuffer buffer = WCharzBuffer(text); 6026 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 6027 } 6028 version(custom_widgets) { 6029 glvw.setItemCount(cast(int) options.length); 6030 //setContentSize(width, cast(int) (options.length * defaultLineHeight)); 6031 redraw(); 6032 } 6033 } 6034 6035 void clear() { 6036 options = null; 6037 version(win32_widgets) { 6038 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 6039 {} 6040 6041 } else version(custom_widgets) { 6042 focusOn = -1; 6043 glvw.setItemCount(0); 6044 redraw(); 6045 } 6046 } 6047 6048 version(custom_widgets) 6049 override void defaultEventHandler_keydown(KeyDownEvent kde) { 6050 void changedFocusOn() { 6051 scrollFocusIntoView(); 6052 if(multiSelect) 6053 redraw(); 6054 else 6055 setSelection(focusOn); 6056 } 6057 switch(kde.key) { 6058 case Key.Up: 6059 if(focusOn) { 6060 focusOn--; 6061 changedFocusOn(); 6062 } 6063 break; 6064 case Key.Down: 6065 if(focusOn + 1 < options.length) { 6066 focusOn++; 6067 changedFocusOn(); 6068 } 6069 break; 6070 case Key.Home: 6071 if(focusOn) { 6072 focusOn = 0; 6073 changedFocusOn(); 6074 } 6075 break; 6076 case Key.End: 6077 if(options.length && focusOn + 1 != options.length) { 6078 focusOn = cast(int) options.length - 1; 6079 changedFocusOn(); 6080 } 6081 break; 6082 case Key.PageUp: 6083 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 6084 focusOn -= n; 6085 if(focusOn < 0) 6086 focusOn = 0; 6087 changedFocusOn(); 6088 break; 6089 case Key.PageDown: 6090 if(options.length == 0) 6091 break; 6092 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 6093 focusOn += n; 6094 if(focusOn >= options.length) 6095 focusOn = cast(int) options.length - 1; 6096 changedFocusOn(); 6097 break; 6098 6099 default: 6100 } 6101 } 6102 6103 version(custom_widgets) 6104 override void defaultEventHandler_char(CharEvent ce) { 6105 if(ce.character == '\n' || ce.character == ' ') { 6106 setSelection(focusOn); 6107 } else { 6108 // search for the item that best matches and jump to it 6109 // FIXME this sucks in tons of ways. the normal thing toolkits 6110 // do here is to search for a substring on a timer, but i'd kinda 6111 // rather make an actual little dialog with some options. still meh for now. 6112 dchar search = ce.character; 6113 if(search >= 'A' && search <= 'Z') 6114 search += 32; 6115 foreach(idx, option; options) { 6116 auto ch = option.label.length ? option.label[0] : 0; 6117 if(ch >= 'A' && ch <= 'Z') 6118 ch += 32; 6119 if(ch == search) { 6120 setSelection(cast(int) idx); 6121 scrollSelectionIntoView(); 6122 break; 6123 } 6124 } 6125 6126 } 6127 } 6128 6129 version(win32_widgets) 6130 enum multiSelect = false; /// not implemented yet 6131 else 6132 bool multiSelect; 6133 6134 override int heightStretchiness() { return 6; } 6135 6136 version(custom_widgets) 6137 void scrollFocusIntoView() { 6138 glvw.ensureItemVisibleInScroll(focusOn); 6139 } 6140 6141 void scrollSelectionIntoView() { 6142 // FIXME: implement on Windows 6143 6144 version(custom_widgets) 6145 glvw.ensureItemVisibleInScroll(getSelection()); 6146 } 6147 6148 /* 6149 version(custom_widgets) 6150 override void defaultEventHandler_focusout(Event foe) { 6151 glvw.redraw(); 6152 } 6153 6154 version(custom_widgets) 6155 override void defaultEventHandler_focusin(Event foe) { 6156 glvw.redraw(); 6157 } 6158 */ 6159 6160 } 6161 6162 6163 6164 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 6165 /// NEVER USED 6166 enum ScrollBarShowPolicy { 6167 automatic, /// automatically show the scroll bar if it is necessary 6168 never, /// never show the scroll bar (scrolling must be done programmatically) 6169 always /// always show the scroll bar, even if it is disabled 6170 } 6171 6172 /++ 6173 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 6174 6175 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 6176 +/ 6177 // FIXME ScrollBarShowPolicy 6178 // FIXME: use the ScrollMessageWidget in here now that it exists 6179 deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it 6180 class ScrollableWidget : Widget { 6181 // FIXME: make line size configurable 6182 // FIXME: add keyboard controls 6183 version(win32_widgets) { 6184 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 6185 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 6186 auto pos = HIWORD(wParam); 6187 auto m = LOWORD(wParam); 6188 6189 // FIXME: I can reintroduce the 6190 // scroll bars now by using this 6191 // in the top-level window handler 6192 // to forward comamnds 6193 auto scrollbarHwnd = lParam; 6194 switch(m) { 6195 case SB_BOTTOM: 6196 if(msg == WM_HSCROLL) 6197 horizontalScrollTo(contentWidth_); 6198 else 6199 verticalScrollTo(contentHeight_); 6200 break; 6201 case SB_TOP: 6202 if(msg == WM_HSCROLL) 6203 horizontalScrollTo(0); 6204 else 6205 verticalScrollTo(0); 6206 break; 6207 case SB_ENDSCROLL: 6208 // idk 6209 break; 6210 case SB_LINEDOWN: 6211 if(msg == WM_HSCROLL) 6212 horizontalScroll(scaleWithDpi(16)); 6213 else 6214 verticalScroll(scaleWithDpi(16)); 6215 break; 6216 case SB_LINEUP: 6217 if(msg == WM_HSCROLL) 6218 horizontalScroll(scaleWithDpi(-16)); 6219 else 6220 verticalScroll(scaleWithDpi(-16)); 6221 break; 6222 case SB_PAGEDOWN: 6223 if(msg == WM_HSCROLL) 6224 horizontalScroll(scaleWithDpi(100)); 6225 else 6226 verticalScroll(scaleWithDpi(100)); 6227 break; 6228 case SB_PAGEUP: 6229 if(msg == WM_HSCROLL) 6230 horizontalScroll(scaleWithDpi(-100)); 6231 else 6232 verticalScroll(scaleWithDpi(-100)); 6233 break; 6234 case SB_THUMBPOSITION: 6235 case SB_THUMBTRACK: 6236 if(msg == WM_HSCROLL) 6237 horizontalScrollTo(pos); 6238 else 6239 verticalScrollTo(pos); 6240 6241 if(m == SB_THUMBTRACK) { 6242 // the event loop doesn't seem to carry on with a requested redraw.. 6243 // so we request it to get our dirty bit set... 6244 redraw(); 6245 6246 // then we need to immediately actually redraw it too for instant feedback to user 6247 6248 SimpleWindow.processAllCustomEvents(); 6249 //if(parentWindow) 6250 //parentWindow.actualRedraw(); 6251 } 6252 break; 6253 default: 6254 } 6255 } 6256 return super.hookedWndProc(msg, wParam, lParam); 6257 } 6258 } 6259 /// 6260 this(Widget parent) { 6261 this.parentWindow = parent.parentWindow; 6262 6263 version(win32_widgets) { 6264 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 6265 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 6266 super(parent); 6267 } else version(custom_widgets) { 6268 outerContainer = new InternalScrollableContainerWidget(this, parent); 6269 super(outerContainer); 6270 } else static assert(0); 6271 } 6272 6273 version(custom_widgets) 6274 InternalScrollableContainerWidget outerContainer; 6275 6276 override void defaultEventHandler_click(ClickEvent event) { 6277 if(event.button == MouseButton.wheelUp) 6278 verticalScroll(scaleWithDpi(-16)); 6279 if(event.button == MouseButton.wheelDown) 6280 verticalScroll(scaleWithDpi(16)); 6281 if(event.button == MouseButton.wheelLeft) 6282 horizontalScroll(scaleWithDpi(-16)); 6283 if(event.button == MouseButton.wheelRight) 6284 horizontalScroll(scaleWithDpi(16)); 6285 super.defaultEventHandler_click(event); 6286 } 6287 6288 override void defaultEventHandler_keydown(KeyDownEvent event) { 6289 switch(event.key) { 6290 case Key.Left: 6291 horizontalScroll(scaleWithDpi(-16)); 6292 break; 6293 case Key.Right: 6294 horizontalScroll(scaleWithDpi(16)); 6295 break; 6296 case Key.Up: 6297 verticalScroll(scaleWithDpi(-16)); 6298 break; 6299 case Key.Down: 6300 verticalScroll(scaleWithDpi(16)); 6301 break; 6302 case Key.Home: 6303 verticalScrollTo(0); 6304 break; 6305 case Key.End: 6306 verticalScrollTo(contentHeight); 6307 break; 6308 case Key.PageUp: 6309 verticalScroll(scaleWithDpi(-160)); 6310 break; 6311 case Key.PageDown: 6312 verticalScroll(scaleWithDpi(160)); 6313 break; 6314 default: 6315 } 6316 super.defaultEventHandler_keydown(event); 6317 } 6318 6319 6320 version(win32_widgets) 6321 override void recomputeChildLayout() { 6322 super.recomputeChildLayout(); 6323 SCROLLINFO info; 6324 info.cbSize = info.sizeof; 6325 info.nPage = viewportHeight; 6326 info.fMask = SIF_PAGE | SIF_RANGE; 6327 info.nMin = 0; 6328 info.nMax = contentHeight_; 6329 SetScrollInfo(hwnd, SB_VERT, &info, true); 6330 6331 info.cbSize = info.sizeof; 6332 info.nPage = viewportWidth; 6333 info.fMask = SIF_PAGE | SIF_RANGE; 6334 info.nMin = 0; 6335 info.nMax = contentWidth_; 6336 SetScrollInfo(hwnd, SB_HORZ, &info, true); 6337 } 6338 6339 /* 6340 Scrolling 6341 ------------ 6342 6343 You are assigned a width and a height by the layout engine, which 6344 is your viewport box. However, you may draw more than that by setting 6345 a contentWidth and contentHeight. 6346 6347 If these can be contained by the viewport, no scrollbar is displayed. 6348 If they cannot fit though, it will automatically show scroll as necessary. 6349 6350 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 6351 is zero, no vertical scrolling is performed. 6352 6353 If scrolling is necessary, the lib will automatically work with the bars. 6354 When you redraw, the origin and clipping info in the painter is set so if 6355 you just draw everything, it will work, but you can be more efficient by checking 6356 the viewportWidth, viewportHeight, and scrollOrigin members. 6357 */ 6358 6359 /// 6360 final @property int viewportWidth() { 6361 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 6362 } 6363 /// 6364 final @property int viewportHeight() { 6365 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 6366 } 6367 6368 // FIXME property 6369 Point scrollOrigin_; 6370 6371 /// 6372 final const(Point) scrollOrigin() { 6373 return scrollOrigin_; 6374 } 6375 6376 // the user sets these two 6377 private int contentWidth_ = 0; 6378 private int contentHeight_ = 0; 6379 6380 /// 6381 int contentWidth() { return contentWidth_; } 6382 /// 6383 int contentHeight() { return contentHeight_; } 6384 6385 /// 6386 void setContentSize(int width, int height) { 6387 contentWidth_ = width; 6388 contentHeight_ = height; 6389 6390 version(custom_widgets) { 6391 if(showingVerticalScroll || showingHorizontalScroll) { 6392 outerContainer.queueRecomputeChildLayout(); 6393 } 6394 6395 if(showingVerticalScroll()) 6396 outerContainer.verticalScrollBar.redraw(); 6397 if(showingHorizontalScroll()) 6398 outerContainer.horizontalScrollBar.redraw(); 6399 } else version(win32_widgets) { 6400 queueRecomputeChildLayout(); 6401 } else static assert(0); 6402 } 6403 6404 /// 6405 void verticalScroll(int delta) { 6406 verticalScrollTo(scrollOrigin.y + delta); 6407 } 6408 /// 6409 void verticalScrollTo(int pos) { 6410 scrollOrigin_.y = pos; 6411 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 6412 scrollOrigin_.y = contentHeight - viewportHeight; 6413 6414 if(scrollOrigin_.y < 0) 6415 scrollOrigin_.y = 0; 6416 6417 version(win32_widgets) { 6418 SCROLLINFO info; 6419 info.cbSize = info.sizeof; 6420 info.fMask = SIF_POS; 6421 info.nPos = scrollOrigin_.y; 6422 SetScrollInfo(hwnd, SB_VERT, &info, true); 6423 } else version(custom_widgets) { 6424 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 6425 } else static assert(0); 6426 6427 redraw(); 6428 } 6429 6430 /// 6431 void horizontalScroll(int delta) { 6432 horizontalScrollTo(scrollOrigin.x + delta); 6433 } 6434 /// 6435 void horizontalScrollTo(int pos) { 6436 scrollOrigin_.x = pos; 6437 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 6438 scrollOrigin_.x = contentWidth - viewportWidth; 6439 6440 if(scrollOrigin_.x < 0) 6441 scrollOrigin_.x = 0; 6442 6443 version(win32_widgets) { 6444 SCROLLINFO info; 6445 info.cbSize = info.sizeof; 6446 info.fMask = SIF_POS; 6447 info.nPos = scrollOrigin_.x; 6448 SetScrollInfo(hwnd, SB_HORZ, &info, true); 6449 } else version(custom_widgets) { 6450 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 6451 } else static assert(0); 6452 6453 redraw(); 6454 } 6455 /// 6456 void scrollTo(Point p) { 6457 verticalScrollTo(p.y); 6458 horizontalScrollTo(p.x); 6459 } 6460 6461 /// 6462 void ensureVisibleInScroll(Point p) { 6463 auto rect = viewportRectangle(); 6464 if(rect.contains(p)) 6465 return; 6466 if(p.x < rect.left) 6467 horizontalScroll(p.x - rect.left); 6468 else if(p.x > rect.right) 6469 horizontalScroll(p.x - rect.right); 6470 6471 if(p.y < rect.top) 6472 verticalScroll(p.y - rect.top); 6473 else if(p.y > rect.bottom) 6474 verticalScroll(p.y - rect.bottom); 6475 } 6476 6477 /// 6478 void ensureVisibleInScroll(Rectangle rect) { 6479 ensureVisibleInScroll(rect.upperLeft); 6480 ensureVisibleInScroll(rect.lowerRight); 6481 } 6482 6483 /// 6484 Rectangle viewportRectangle() { 6485 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 6486 } 6487 6488 /// 6489 bool showingHorizontalScroll() { 6490 return contentWidth > width; 6491 } 6492 /// 6493 bool showingVerticalScroll() { 6494 return contentHeight > height; 6495 } 6496 6497 /// This is called before the ordinary paint delegate, 6498 /// giving you a chance to draw the window frame, etc, 6499 /// before the scroll clip takes effect 6500 void paintFrameAndBackground(WidgetPainter painter) { 6501 version(win32_widgets) { 6502 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 6503 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 6504 // since the pen is null, to fill the whole space, we need the +1 on both. 6505 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 6506 SelectObject(painter.impl.hdc, p); 6507 SelectObject(painter.impl.hdc, b); 6508 } 6509 6510 } 6511 6512 // make space for the scroll bar, and that's it. 6513 final override int paddingRight() { return scaleWithDpi(16); } 6514 final override int paddingBottom() { return scaleWithDpi(16); } 6515 6516 /* 6517 END SCROLLING 6518 */ 6519 6520 override WidgetPainter draw() { 6521 int x = this.x, y = this.y; 6522 auto parent = this.parent; 6523 while(parent) { 6524 x += parent.x; 6525 y += parent.y; 6526 parent = parent.parent; 6527 } 6528 6529 //version(win32_widgets) { 6530 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6531 //} else { 6532 auto painter = parentWindow.win.draw(true); 6533 //} 6534 painter.originX = x; 6535 painter.originY = y; 6536 6537 painter.originX = painter.originX - scrollOrigin.x; 6538 painter.originY = painter.originY - scrollOrigin.y; 6539 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 6540 6541 return WidgetPainter(painter, this); 6542 } 6543 6544 override void addScrollPosition(ref int x, ref int y) { 6545 x += scrollOrigin.x; 6546 y += scrollOrigin.y; 6547 } 6548 6549 mixin ScrollableChildren; 6550 } 6551 6552 // you need to have a Point scrollOrigin in the class somewhere 6553 // and a paintFrameAndBackground 6554 private mixin template ScrollableChildren() { 6555 static assert(!__traits(isSame, this.addScrollPosition, Widget.addScrollPosition), "Your widget should provide `Point scrollOrigin()` and `override void addScrollPosition`"); 6556 6557 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6558 if(hidden) 6559 return; 6560 6561 //version(win32_widgets) 6562 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6563 6564 painter.originX = lox + x; 6565 painter.originY = loy + y; 6566 6567 bool actuallyPainted = false; 6568 6569 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 6570 if(clip == Rectangle.init) 6571 return; 6572 6573 if(force || redrawRequested) { 6574 //painter.setClipRectangle(scrollOrigin, width, height); 6575 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6576 paintFrameAndBackground(painter); 6577 } 6578 6579 /+ 6580 version(win32_widgets) { 6581 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 6582 } 6583 +/ 6584 6585 painter.originX = painter.originX - scrollOrigin.x; 6586 painter.originY = painter.originY - scrollOrigin.y; 6587 if(force || redrawRequested) { 6588 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 6589 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 6590 6591 //erase(painter); // we paintFrameAndBackground above so no need 6592 if(painter.visualTheme) 6593 painter.visualTheme.doPaint(this, painter); 6594 else 6595 paint(painter); 6596 6597 if(invalidate) { 6598 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6599 // children are contained inside this, so no need to do extra work 6600 invalidate = false; 6601 } 6602 6603 6604 actuallyPainted = true; 6605 redrawRequested = false; 6606 } 6607 6608 foreach(child; children) { 6609 if(cast(FixedPosition) child) 6610 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6611 else 6612 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6613 } 6614 } 6615 } 6616 6617 private class InternalScrollableContainerInsideWidget : ContainerWidget { 6618 ScrollableContainerWidget scw; 6619 6620 this(ScrollableContainerWidget parent) { 6621 scw = parent; 6622 super(parent); 6623 } 6624 6625 version(custom_widgets) 6626 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6627 if(hidden) 6628 return; 6629 6630 bool actuallyPainted = false; 6631 6632 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 6633 6634 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 6635 if(clip == Rectangle.init) 6636 return; 6637 6638 painter.originX = lox + x - scrollOrigin.x; 6639 painter.originY = loy + y - scrollOrigin.y; 6640 if(force || redrawRequested) { 6641 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6642 6643 erase(painter); 6644 if(painter.visualTheme) 6645 painter.visualTheme.doPaint(this, painter); 6646 else 6647 paint(painter); 6648 6649 if(invalidate) { 6650 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6651 // children are contained inside this, so no need to do extra work 6652 invalidate = false; 6653 } 6654 6655 actuallyPainted = true; 6656 redrawRequested = false; 6657 } 6658 foreach(child; children) { 6659 if(cast(FixedPosition) child) 6660 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6661 else 6662 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6663 } 6664 } 6665 6666 version(custom_widgets) 6667 override protected void addScrollPosition(ref int x, ref int y) { 6668 x += scw.scrollX_; 6669 y += scw.scrollY_; 6670 } 6671 } 6672 6673 /++ 6674 A widget meant to contain other widgets that may need to scroll. 6675 6676 Currently buggy. 6677 6678 History: 6679 Added July 1, 2021 (dub v10.2) 6680 6681 On January 3, 2022, I tried to use it in a few other cases 6682 and found it only worked well in the original test case. Since 6683 it still sucks, I think I'm going to rewrite it again. 6684 +/ 6685 class ScrollableContainerWidget : ContainerWidget { 6686 /// 6687 this(Widget parent) { 6688 super(parent); 6689 6690 container = new InternalScrollableContainerInsideWidget(this); 6691 hsb = new HorizontalScrollbar(this); 6692 vsb = new VerticalScrollbar(this); 6693 6694 tabStop = false; 6695 container.tabStop = false; 6696 magic = true; 6697 6698 6699 vsb.addEventListener("scrolltonextline", () { 6700 scrollBy(0, scaleWithDpi(16)); 6701 }); 6702 vsb.addEventListener("scrolltopreviousline", () { 6703 scrollBy(0,scaleWithDpi( -16)); 6704 }); 6705 vsb.addEventListener("scrolltonextpage", () { 6706 scrollBy(0, container.height); 6707 }); 6708 vsb.addEventListener("scrolltopreviouspage", () { 6709 scrollBy(0, -container.height); 6710 }); 6711 vsb.addEventListener((scope ScrollToPositionEvent spe) { 6712 scrollTo(scrollX_, spe.value); 6713 }); 6714 6715 this.addEventListener(delegate (scope ClickEvent e) { 6716 if(e.button == MouseButton.wheelUp) { 6717 if(!e.defaultPrevented) 6718 scrollBy(0, scaleWithDpi(-16)); 6719 e.stopPropagation(); 6720 } else if(e.button == MouseButton.wheelDown) { 6721 if(!e.defaultPrevented) 6722 scrollBy(0, scaleWithDpi(16)); 6723 e.stopPropagation(); 6724 } else if(e.button == MouseButton.wheelLeft) { 6725 if(!e.defaultPrevented) 6726 scrollBy(scaleWithDpi(-16), 0); 6727 e.stopPropagation(); 6728 } else if(e.button == MouseButton.wheelRight) { 6729 if(!e.defaultPrevented) 6730 scrollBy(scaleWithDpi(16), 0); 6731 e.stopPropagation(); 6732 } 6733 }); 6734 } 6735 6736 /+ 6737 override void defaultEventHandler_click(ClickEvent e) { 6738 } 6739 +/ 6740 6741 override void removeAllChildren() { 6742 container.removeAllChildren(); 6743 } 6744 6745 void scrollTo(int x, int y) { 6746 scrollBy(x - scrollX_, y - scrollY_); 6747 } 6748 6749 void scrollBy(int x, int y) { 6750 auto ox = scrollX_; 6751 auto oy = scrollY_; 6752 6753 auto nx = ox + x; 6754 auto ny = oy + y; 6755 6756 if(nx < 0) 6757 nx = 0; 6758 if(ny < 0) 6759 ny = 0; 6760 6761 auto maxX = hsb.max - container.width; 6762 if(maxX < 0) maxX = 0; 6763 auto maxY = vsb.max - container.height; 6764 if(maxY < 0) maxY = 0; 6765 6766 if(nx > maxX) 6767 nx = maxX; 6768 if(ny > maxY) 6769 ny = maxY; 6770 6771 auto dx = nx - ox; 6772 auto dy = ny - oy; 6773 6774 if(dx || dy) { 6775 version(win32_widgets) 6776 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 6777 else { 6778 redraw(); 6779 } 6780 6781 hsb.setPosition = nx; 6782 vsb.setPosition = ny; 6783 6784 scrollX_ = nx; 6785 scrollY_ = ny; 6786 } 6787 } 6788 6789 private int scrollX_; 6790 private int scrollY_; 6791 6792 void setTotalArea(int width, int height) { 6793 hsb.setMax(width); 6794 vsb.setMax(height); 6795 } 6796 6797 /// 6798 void setViewableArea(int width, int height) { 6799 hsb.setViewableArea(width); 6800 vsb.setViewableArea(height); 6801 } 6802 6803 private bool magic; 6804 override void addChild(Widget w, int position = int.max) { 6805 if(magic) 6806 container.addChild(w, position); 6807 else 6808 super.addChild(w, position); 6809 } 6810 6811 override void recomputeChildLayout() { 6812 if(hsb is null || vsb is null || container is null) return; 6813 6814 /+ 6815 writeln(x, " ", y , " ", width, " ", height); 6816 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 6817 +/ 6818 6819 registerMovement(); 6820 6821 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 6822 hsb.x = 0; 6823 hsb.y = this.height - hsb.height; 6824 hsb.width = this.width - scaleWithDpi(16); 6825 hsb.recomputeChildLayout(); 6826 6827 vsb.width = scaleWithDpi(16); // FIXME? 6828 vsb.x = this.width - vsb.width; 6829 vsb.y = 0; 6830 vsb.height = this.height - scaleWithDpi(16); 6831 vsb.recomputeChildLayout(); 6832 6833 container.x = 0; 6834 container.y = 0; 6835 container.width = this.width - vsb.width; 6836 container.height = this.height - hsb.height; 6837 container.recomputeChildLayout(); 6838 6839 scrollX_ = 0; 6840 scrollY_ = 0; 6841 6842 hsb.setPosition(0); 6843 vsb.setPosition(0); 6844 6845 int mw, mh; 6846 Widget c = container; 6847 // FIXME: hack here to handle a layout inside... 6848 if(c.children.length == 1 && cast(Layout) c.children[0]) 6849 c = c.children[0]; 6850 foreach(child; c.children) { 6851 auto w = child.x + child.width; 6852 auto h = child.y + child.height; 6853 6854 if(w > mw) mw = w; 6855 if(h > mh) mh = h; 6856 } 6857 6858 setTotalArea(mw, mh); 6859 setViewableArea(width, height); 6860 } 6861 6862 override int minHeight() { return scaleWithDpi(64); } 6863 6864 HorizontalScrollbar hsb; 6865 VerticalScrollbar vsb; 6866 ContainerWidget container; 6867 } 6868 6869 6870 version(custom_widgets) 6871 deprecated 6872 private class InternalScrollableContainerWidget : Widget { 6873 6874 ScrollableWidget sw; 6875 6876 VerticalScrollbar verticalScrollBar; 6877 HorizontalScrollbar horizontalScrollBar; 6878 6879 this(ScrollableWidget sw, Widget parent) { 6880 this.sw = sw; 6881 6882 this.tabStop = false; 6883 6884 super(parent); 6885 6886 horizontalScrollBar = new HorizontalScrollbar(this); 6887 verticalScrollBar = new VerticalScrollbar(this); 6888 6889 horizontalScrollBar.showing_ = false; 6890 verticalScrollBar.showing_ = false; 6891 6892 horizontalScrollBar.addEventListener("scrolltonextline", { 6893 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 6894 sw.horizontalScrollTo(horizontalScrollBar.position); 6895 }); 6896 horizontalScrollBar.addEventListener("scrolltopreviousline", { 6897 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 6898 sw.horizontalScrollTo(horizontalScrollBar.position); 6899 }); 6900 verticalScrollBar.addEventListener("scrolltonextline", { 6901 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 6902 sw.verticalScrollTo(verticalScrollBar.position); 6903 }); 6904 verticalScrollBar.addEventListener("scrolltopreviousline", { 6905 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 6906 sw.verticalScrollTo(verticalScrollBar.position); 6907 }); 6908 horizontalScrollBar.addEventListener("scrolltonextpage", { 6909 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 6910 sw.horizontalScrollTo(horizontalScrollBar.position); 6911 }); 6912 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 6913 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 6914 sw.horizontalScrollTo(horizontalScrollBar.position); 6915 }); 6916 verticalScrollBar.addEventListener("scrolltonextpage", { 6917 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 6918 sw.verticalScrollTo(verticalScrollBar.position); 6919 }); 6920 verticalScrollBar.addEventListener("scrolltopreviouspage", { 6921 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 6922 sw.verticalScrollTo(verticalScrollBar.position); 6923 }); 6924 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 6925 horizontalScrollBar.setPosition(event.intValue); 6926 sw.horizontalScrollTo(horizontalScrollBar.position); 6927 }); 6928 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 6929 verticalScrollBar.setPosition(event.intValue); 6930 sw.verticalScrollTo(verticalScrollBar.position); 6931 }); 6932 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 6933 horizontalScrollBar.setPosition(event.intValue); 6934 sw.horizontalScrollTo(horizontalScrollBar.position); 6935 }); 6936 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 6937 verticalScrollBar.setPosition(event.intValue); 6938 }); 6939 } 6940 6941 // this is supposed to be basically invisible... 6942 override int minWidth() { return sw.minWidth; } 6943 override int minHeight() { return sw.minHeight; } 6944 override int maxWidth() { return sw.maxWidth; } 6945 override int maxHeight() { return sw.maxHeight; } 6946 override int widthStretchiness() { return sw.widthStretchiness; } 6947 override int heightStretchiness() { return sw.heightStretchiness; } 6948 override int marginLeft() { return sw.marginLeft; } 6949 override int marginRight() { return sw.marginRight; } 6950 override int marginTop() { return sw.marginTop; } 6951 override int marginBottom() { return sw.marginBottom; } 6952 override int paddingLeft() { return sw.paddingLeft; } 6953 override int paddingRight() { return sw.paddingRight; } 6954 override int paddingTop() { return sw.paddingTop; } 6955 override int paddingBottom() { return sw.paddingBottom; } 6956 override void focus() { sw.focus(); } 6957 6958 6959 override void recomputeChildLayout() { 6960 // The stupid thing needs to calculate if a scroll bar is needed... 6961 recomputeChildLayoutHelper(); 6962 // then running it again will position things correctly if the bar is NOT needed 6963 recomputeChildLayoutHelper(); 6964 6965 // this sucks but meh it barely works 6966 } 6967 6968 private void recomputeChildLayoutHelper() { 6969 if(sw is null) return; 6970 6971 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 6972 if(horizontalScrollBar && verticalScrollBar) { 6973 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 6974 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 6975 horizontalScrollBar.x = 0; 6976 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 6977 6978 verticalScrollBar.width = verticalScrollBar.minWidth(); 6979 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 6980 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 6981 verticalScrollBar.y = 0 + 2; 6982 6983 sw.x = 0; 6984 sw.y = 0; 6985 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 6986 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 6987 6988 if(sw.contentWidth_ <= this.width) 6989 sw.scrollOrigin_.x = 0; 6990 if(sw.contentHeight_ <= this.height) 6991 sw.scrollOrigin_.y = 0; 6992 6993 horizontalScrollBar.recomputeChildLayout(); 6994 verticalScrollBar.recomputeChildLayout(); 6995 sw.recomputeChildLayout(); 6996 } 6997 6998 if(sw.contentWidth_ <= this.width) 6999 sw.scrollOrigin_.x = 0; 7000 if(sw.contentHeight_ <= this.height) 7001 sw.scrollOrigin_.y = 0; 7002 7003 if(sw.showingHorizontalScroll()) 7004 horizontalScrollBar.showing(true, false); 7005 else 7006 horizontalScrollBar.showing(false, false); 7007 if(sw.showingVerticalScroll()) 7008 verticalScrollBar.showing(true, false); 7009 else 7010 verticalScrollBar.showing(false, false); 7011 7012 verticalScrollBar.setViewableArea(sw.viewportHeight()); 7013 verticalScrollBar.setMax(sw.contentHeight); 7014 verticalScrollBar.setPosition(sw.scrollOrigin.y); 7015 7016 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 7017 horizontalScrollBar.setMax(sw.contentWidth); 7018 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 7019 } 7020 } 7021 7022 /* 7023 class ScrollableClientWidget : Widget { 7024 this(Widget parent) { 7025 super(parent); 7026 } 7027 override void paint(WidgetPainter p) { 7028 parent.paint(p); 7029 } 7030 } 7031 */ 7032 7033 /++ 7034 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. 7035 +/ 7036 abstract class Slider : Widget { 7037 this(int min, int max, int step, Widget parent) { 7038 min_ = min; 7039 max_ = max; 7040 step_ = step; 7041 page_ = step; 7042 super(parent); 7043 } 7044 7045 private int min_; 7046 private int max_; 7047 private int step_; 7048 private int position_; 7049 private int page_; 7050 7051 // selection start and selection end 7052 // tics 7053 // tooltip? 7054 // some way to see and just type the value 7055 // win32 buddy controls are labels 7056 7057 /// 7058 void setMin(int a) { 7059 min_ = a; 7060 version(custom_widgets) 7061 redraw(); 7062 version(win32_widgets) 7063 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 7064 } 7065 /// 7066 int min() { 7067 return min_; 7068 } 7069 /// 7070 void setMax(int a) { 7071 max_ = a; 7072 version(custom_widgets) 7073 redraw(); 7074 version(win32_widgets) 7075 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 7076 } 7077 /// 7078 int max() { 7079 return max_; 7080 } 7081 /// 7082 void setPosition(int a) { 7083 if(a > max) 7084 a = max; 7085 if(a < min) 7086 a = min; 7087 position_ = a; 7088 version(custom_widgets) 7089 setPositionCustom(a); 7090 7091 version(win32_widgets) 7092 setPositionWindows(a); 7093 } 7094 version(win32_widgets) { 7095 protected abstract void setPositionWindows(int a); 7096 } 7097 7098 protected abstract int win32direction(); 7099 7100 /++ 7101 Alias for [position] for better compatibility with generic code. 7102 7103 History: 7104 Added October 5, 2021 7105 +/ 7106 @property int value() { 7107 return position; 7108 } 7109 7110 /// 7111 int position() { 7112 return position_; 7113 } 7114 /// 7115 void setStep(int a) { 7116 step_ = a; 7117 version(win32_widgets) 7118 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 7119 } 7120 /// 7121 int step() { 7122 return step_; 7123 } 7124 /// 7125 void setPageSize(int a) { 7126 page_ = a; 7127 version(win32_widgets) 7128 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 7129 } 7130 /// 7131 int pageSize() { 7132 return page_; 7133 } 7134 7135 private void notify() { 7136 auto event = new ChangeEvent!int(this, &this.position); 7137 event.dispatch(); 7138 } 7139 7140 version(win32_widgets) 7141 void win32Setup(int style) { 7142 createWin32Window(this, TRACKBAR_CLASS, "", 7143 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 7144 7145 // the trackbar sends the same messages as scroll, which 7146 // our other layer sends as these... just gonna translate 7147 // here 7148 this.addDirectEventListener("scrolltoposition", (Event event) { 7149 event.stopPropagation(); 7150 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 7151 notify(); 7152 }); 7153 this.addDirectEventListener("scrolltonextline", (Event event) { 7154 event.stopPropagation(); 7155 this.setPosition(this.position + this.step_ * this.win32direction); 7156 notify(); 7157 }); 7158 this.addDirectEventListener("scrolltopreviousline", (Event event) { 7159 event.stopPropagation(); 7160 this.setPosition(this.position - this.step_ * this.win32direction); 7161 notify(); 7162 }); 7163 this.addDirectEventListener("scrolltonextpage", (Event event) { 7164 event.stopPropagation(); 7165 this.setPosition(this.position + this.page_ * this.win32direction); 7166 notify(); 7167 }); 7168 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 7169 event.stopPropagation(); 7170 this.setPosition(this.position - this.page_ * this.win32direction); 7171 notify(); 7172 }); 7173 7174 setMin(min_); 7175 setMax(max_); 7176 setStep(step_); 7177 setPageSize(page_); 7178 } 7179 7180 version(custom_widgets) { 7181 protected MouseTrackingWidget thumb; 7182 7183 protected abstract void setPositionCustom(int a); 7184 7185 override void defaultEventHandler_keydown(KeyDownEvent event) { 7186 switch(event.key) { 7187 case Key.Up: 7188 case Key.Right: 7189 setPosition(position() - step() * win32direction); 7190 changed(); 7191 break; 7192 case Key.Down: 7193 case Key.Left: 7194 setPosition(position() + step() * win32direction); 7195 changed(); 7196 break; 7197 case Key.Home: 7198 setPosition(win32direction > 0 ? min() : max()); 7199 changed(); 7200 break; 7201 case Key.End: 7202 setPosition(win32direction > 0 ? max() : min()); 7203 changed(); 7204 break; 7205 case Key.PageUp: 7206 setPosition(position() - pageSize() * win32direction); 7207 changed(); 7208 break; 7209 case Key.PageDown: 7210 setPosition(position() + pageSize() * win32direction); 7211 changed(); 7212 break; 7213 default: 7214 } 7215 super.defaultEventHandler_keydown(event); 7216 } 7217 7218 protected void changed() { 7219 auto ev = new ChangeEvent!int(this, &position); 7220 ev.dispatch(); 7221 } 7222 } 7223 } 7224 7225 /++ 7226 7227 +/ 7228 class VerticalSlider : Slider { 7229 this(int min, int max, int step, Widget parent) { 7230 version(custom_widgets) 7231 initialize(); 7232 7233 super(min, max, step, parent); 7234 7235 version(win32_widgets) 7236 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 7237 } 7238 7239 protected override int win32direction() { 7240 return -1; 7241 } 7242 7243 version(win32_widgets) 7244 protected override void setPositionWindows(int a) { 7245 // the windows thing makes the top 0 and i don't like that. 7246 SendMessage(hwnd, TBM_SETPOS, true, max - a); 7247 } 7248 7249 version(custom_widgets) 7250 private void initialize() { 7251 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 7252 7253 thumb.tabStop = false; 7254 7255 thumb.thumbWidth = width; 7256 thumb.thumbHeight = scaleWithDpi(16); 7257 7258 thumb.addEventListener(EventType.change, () { 7259 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 7260 sx = max - sx; 7261 //informProgramThatUserChangedPosition(sx); 7262 7263 position_ = sx; 7264 7265 changed(); 7266 }); 7267 } 7268 7269 version(custom_widgets) 7270 override void recomputeChildLayout() { 7271 thumb.thumbWidth = this.width; 7272 super.recomputeChildLayout(); 7273 setPositionCustom(position_); 7274 } 7275 7276 version(custom_widgets) 7277 protected override void setPositionCustom(int a) { 7278 if(max()) 7279 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 7280 redraw(); 7281 } 7282 } 7283 7284 /++ 7285 7286 +/ 7287 class HorizontalSlider : Slider { 7288 this(int min, int max, int step, Widget parent) { 7289 version(custom_widgets) 7290 initialize(); 7291 7292 super(min, max, step, parent); 7293 7294 version(win32_widgets) 7295 win32Setup(TBS_HORZ); 7296 } 7297 7298 version(win32_widgets) 7299 protected override void setPositionWindows(int a) { 7300 SendMessage(hwnd, TBM_SETPOS, true, a); 7301 } 7302 7303 protected override int win32direction() { 7304 return 1; 7305 } 7306 7307 version(custom_widgets) 7308 private void initialize() { 7309 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 7310 7311 thumb.tabStop = false; 7312 7313 thumb.thumbWidth = scaleWithDpi(16); 7314 thumb.thumbHeight = height; 7315 7316 thumb.addEventListener(EventType.change, () { 7317 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 7318 //informProgramThatUserChangedPosition(sx); 7319 7320 position_ = sx; 7321 7322 changed(); 7323 }); 7324 } 7325 7326 version(custom_widgets) 7327 override void recomputeChildLayout() { 7328 thumb.thumbHeight = this.height; 7329 super.recomputeChildLayout(); 7330 setPositionCustom(position_); 7331 } 7332 7333 version(custom_widgets) 7334 protected override void setPositionCustom(int a) { 7335 if(max()) 7336 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 7337 redraw(); 7338 } 7339 } 7340 7341 7342 /// 7343 abstract class ScrollbarBase : Widget { 7344 /// 7345 this(Widget parent) { 7346 super(parent); 7347 tabStop = false; 7348 step_ = scaleWithDpi(16); 7349 } 7350 7351 private int viewableArea_; 7352 private int max_; 7353 private int step_;// = 16; 7354 private int position_; 7355 7356 /// 7357 bool atEnd() { 7358 return position_ + viewableArea_ >= max_; 7359 } 7360 7361 /// 7362 bool atStart() { 7363 return position_ == 0; 7364 } 7365 7366 /// 7367 void setViewableArea(int a) { 7368 viewableArea_ = a; 7369 version(custom_widgets) 7370 redraw(); 7371 } 7372 /// 7373 void setMax(int a) { 7374 max_ = a; 7375 version(custom_widgets) 7376 redraw(); 7377 } 7378 /// 7379 int max() { 7380 return max_; 7381 } 7382 /// 7383 void setPosition(int a) { 7384 auto logicalMax = max_ - viewableArea_; 7385 if(a == int.max) 7386 a = logicalMax; 7387 7388 if(a > logicalMax) 7389 a = logicalMax; 7390 if(a < 0) 7391 a = 0; 7392 7393 position_ = a; 7394 7395 version(custom_widgets) 7396 redraw(); 7397 } 7398 /// 7399 int position() { 7400 return position_; 7401 } 7402 /// 7403 void setStep(int a) { 7404 step_ = a; 7405 } 7406 /// 7407 int step() { 7408 return step_; 7409 } 7410 7411 // FIXME: remove this.... maybe 7412 /+ 7413 protected void informProgramThatUserChangedPosition(int n) { 7414 position_ = n; 7415 auto evt = new Event(EventType.change, this); 7416 evt.intValue = n; 7417 evt.dispatch(); 7418 } 7419 +/ 7420 7421 version(custom_widgets) { 7422 enum MIN_THUMB_SIZE = 8; 7423 7424 abstract protected int getBarDim(); 7425 int thumbSize() { 7426 if(viewableArea_ >= max_ || max_ == 0) 7427 return getBarDim(); 7428 7429 int res = viewableArea_ * getBarDim() / max_; 7430 7431 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 7432 res = scaleWithDpi(MIN_THUMB_SIZE); 7433 7434 return res; 7435 } 7436 7437 int thumbPosition() { 7438 /* 7439 viewableArea_ is the viewport height/width 7440 position_ is where we are 7441 */ 7442 //if(position_ + viewableArea_ >= max_) 7443 //return getBarDim - thumbSize; 7444 7445 auto maximumPossibleValue = getBarDim() - thumbSize; 7446 auto maximiumLogicalValue = max_ - viewableArea_; 7447 7448 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 7449 7450 return p; 7451 } 7452 } 7453 } 7454 7455 //public import mgt; 7456 7457 /++ 7458 A mouse tracking widget is one that follows the mouse when dragged inside it. 7459 7460 Concrete subclasses may include a scrollbar thumb and a volume control. 7461 +/ 7462 //version(custom_widgets) 7463 class MouseTrackingWidget : Widget { 7464 7465 /// 7466 int positionX() { return positionX_; } 7467 /// 7468 int positionY() { return positionY_; } 7469 7470 /// 7471 void positionX(int p) { positionX_ = p; } 7472 /// 7473 void positionY(int p) { positionY_ = p; } 7474 7475 private int positionX_; 7476 private int positionY_; 7477 7478 /// 7479 enum Orientation { 7480 horizontal, /// 7481 vertical, /// 7482 twoDimensional, /// 7483 } 7484 7485 private int thumbWidth_; 7486 private int thumbHeight_; 7487 7488 /// 7489 int thumbWidth() { return thumbWidth_; } 7490 /// 7491 int thumbHeight() { return thumbHeight_; } 7492 /// 7493 int thumbWidth(int a) { return thumbWidth_ = a; } 7494 /// 7495 int thumbHeight(int a) { return thumbHeight_ = a; } 7496 7497 private bool dragging; 7498 private bool hovering; 7499 private int startMouseX, startMouseY; 7500 7501 /// 7502 this(Orientation orientation, Widget parent) { 7503 super(parent); 7504 7505 //assert(parentWindow !is null); 7506 7507 addEventListener((MouseDownEvent event) { 7508 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7509 dragging = true; 7510 startMouseX = event.clientX - positionX; 7511 startMouseY = event.clientY - positionY; 7512 parentWindow.captureMouse(this); 7513 } else { 7514 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7515 positionX = event.clientX - thumbWidth / 2; 7516 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7517 positionY = event.clientY - thumbHeight / 2; 7518 7519 if(positionX + thumbWidth > this.width) 7520 positionX = this.width - thumbWidth; 7521 if(positionY + thumbHeight > this.height) 7522 positionY = this.height - thumbHeight; 7523 7524 if(positionX < 0) 7525 positionX = 0; 7526 if(positionY < 0) 7527 positionY = 0; 7528 7529 7530 // this.emit!(ChangeEvent!void)(); 7531 auto evt = new Event(EventType.change, this); 7532 evt.sendDirectly(); 7533 7534 redraw(); 7535 7536 } 7537 }); 7538 7539 addEventListener(EventType.mouseup, (Event event) { 7540 dragging = false; 7541 parentWindow.releaseMouseCapture(); 7542 }); 7543 7544 addEventListener(EventType.mouseout, (Event event) { 7545 if(!hovering) 7546 return; 7547 hovering = false; 7548 redraw(); 7549 }); 7550 7551 int lpx, lpy; 7552 7553 addEventListener((MouseMoveEvent event) { 7554 auto oh = hovering; 7555 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7556 hovering = true; 7557 } else { 7558 hovering = false; 7559 } 7560 if(!dragging) { 7561 if(hovering != oh) 7562 redraw(); 7563 return; 7564 } 7565 7566 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7567 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 7568 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7569 positionY = event.clientY - startMouseY; 7570 7571 if(positionX + thumbWidth > this.width) 7572 positionX = this.width - thumbWidth; 7573 if(positionY + thumbHeight > this.height) 7574 positionY = this.height - thumbHeight; 7575 7576 if(positionX < 0) 7577 positionX = 0; 7578 if(positionY < 0) 7579 positionY = 0; 7580 7581 if(positionX != lpx || positionY != lpy) { 7582 lpx = positionX; 7583 lpy = positionY; 7584 7585 auto evt = new Event(EventType.change, this); 7586 evt.sendDirectly(); 7587 } 7588 7589 redraw(); 7590 }); 7591 } 7592 7593 version(custom_widgets) 7594 override void paint(WidgetPainter painter) { 7595 auto cs = getComputedStyle(); 7596 auto c = darken(cs.windowBackgroundColor, 0.2); 7597 painter.outlineColor = c; 7598 painter.fillColor = c; 7599 painter.drawRectangle(Point(0, 0), this.width, this.height); 7600 7601 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 7602 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 7603 } 7604 } 7605 7606 //version(custom_widgets) 7607 //private 7608 class HorizontalScrollbar : ScrollbarBase { 7609 7610 version(custom_widgets) { 7611 private MouseTrackingWidget thumb; 7612 7613 override int getBarDim() { 7614 return thumb.width; 7615 } 7616 } 7617 7618 override void setViewableArea(int a) { 7619 super.setViewableArea(a); 7620 7621 version(win32_widgets) { 7622 SCROLLINFO info; 7623 info.cbSize = info.sizeof; 7624 info.nPage = a + 1; 7625 info.fMask = SIF_PAGE; 7626 SetScrollInfo(hwnd, SB_CTL, &info, true); 7627 } else version(custom_widgets) { 7628 thumb.positionX = thumbPosition; 7629 thumb.thumbWidth = thumbSize; 7630 thumb.redraw(); 7631 } else static assert(0); 7632 7633 } 7634 7635 override void setMax(int a) { 7636 super.setMax(a); 7637 version(win32_widgets) { 7638 SCROLLINFO info; 7639 info.cbSize = info.sizeof; 7640 info.nMin = 0; 7641 info.nMax = max; 7642 info.fMask = SIF_RANGE; 7643 SetScrollInfo(hwnd, SB_CTL, &info, true); 7644 } else version(custom_widgets) { 7645 thumb.positionX = thumbPosition; 7646 thumb.thumbWidth = thumbSize; 7647 thumb.redraw(); 7648 } 7649 } 7650 7651 override void setPosition(int a) { 7652 super.setPosition(a); 7653 version(win32_widgets) { 7654 SCROLLINFO info; 7655 info.cbSize = info.sizeof; 7656 info.fMask = SIF_POS; 7657 info.nPos = position; 7658 SetScrollInfo(hwnd, SB_CTL, &info, true); 7659 } else version(custom_widgets) { 7660 thumb.positionX = thumbPosition(); 7661 thumb.thumbWidth = thumbSize; 7662 thumb.redraw(); 7663 } else static assert(0); 7664 } 7665 7666 this(Widget parent) { 7667 super(parent); 7668 7669 version(win32_widgets) { 7670 createWin32Window(this, "Scrollbar"w, "", 7671 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 7672 } else version(custom_widgets) { 7673 auto vl = new HorizontalLayout(this); 7674 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 7675 leftButton.setClickRepeat(scrollClickRepeatInterval); 7676 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 7677 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 7678 rightButton.setClickRepeat(scrollClickRepeatInterval); 7679 7680 leftButton.tabStop = false; 7681 rightButton.tabStop = false; 7682 thumb.tabStop = false; 7683 7684 leftButton.addEventListener(EventType.triggered, () { 7685 this.emitCommand!"scrolltopreviousline"(); 7686 //informProgramThatUserChangedPosition(position - step()); 7687 }); 7688 rightButton.addEventListener(EventType.triggered, () { 7689 this.emitCommand!"scrolltonextline"(); 7690 //informProgramThatUserChangedPosition(position + step()); 7691 }); 7692 7693 thumb.thumbWidth = this.minWidth; 7694 thumb.thumbHeight = scaleWithDpi(16); 7695 7696 thumb.addEventListener(EventType.change, () { 7697 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 7698 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 7699 7700 //informProgramThatUserChangedPosition(sx); 7701 7702 auto ev = new ScrollToPositionEvent(this, sx); 7703 ev.dispatch(); 7704 }); 7705 } 7706 } 7707 7708 version(custom_widgets) 7709 override void dpiChanged() { 7710 thumb.thumbHeight = scaleWithDpi(16); 7711 } 7712 7713 override int minHeight() { return scaleWithDpi(16); } 7714 override int maxHeight() { return scaleWithDpi(16); } 7715 override int minWidth() { return scaleWithDpi(48); } 7716 } 7717 7718 final class ScrollToPositionEvent : Event { 7719 enum EventString = "scrolltoposition"; 7720 7721 this(Widget target, int value) { 7722 this.value = value; 7723 super(EventString, target); 7724 } 7725 7726 immutable int value; 7727 7728 override @property int intValue() { 7729 return value; 7730 } 7731 } 7732 7733 //version(custom_widgets) 7734 //private 7735 class VerticalScrollbar : ScrollbarBase { 7736 7737 version(custom_widgets) { 7738 override int getBarDim() { 7739 return thumb.height; 7740 } 7741 7742 private MouseTrackingWidget thumb; 7743 } 7744 7745 override void setViewableArea(int a) { 7746 super.setViewableArea(a); 7747 7748 version(win32_widgets) { 7749 SCROLLINFO info; 7750 info.cbSize = info.sizeof; 7751 info.nPage = a + 1; 7752 info.fMask = SIF_PAGE; 7753 SetScrollInfo(hwnd, SB_CTL, &info, true); 7754 } else version(custom_widgets) { 7755 thumb.positionY = thumbPosition; 7756 thumb.thumbHeight = thumbSize; 7757 thumb.redraw(); 7758 } else static assert(0); 7759 7760 } 7761 7762 override void setMax(int a) { 7763 super.setMax(a); 7764 version(win32_widgets) { 7765 SCROLLINFO info; 7766 info.cbSize = info.sizeof; 7767 info.nMin = 0; 7768 info.nMax = max; 7769 info.fMask = SIF_RANGE; 7770 SetScrollInfo(hwnd, SB_CTL, &info, true); 7771 } else version(custom_widgets) { 7772 thumb.positionY = thumbPosition; 7773 thumb.thumbHeight = thumbSize; 7774 thumb.redraw(); 7775 } 7776 } 7777 7778 override void setPosition(int a) { 7779 super.setPosition(a); 7780 version(win32_widgets) { 7781 SCROLLINFO info; 7782 info.cbSize = info.sizeof; 7783 info.fMask = SIF_POS; 7784 info.nPos = position; 7785 SetScrollInfo(hwnd, SB_CTL, &info, true); 7786 } else version(custom_widgets) { 7787 thumb.positionY = thumbPosition; 7788 thumb.thumbHeight = thumbSize; 7789 thumb.redraw(); 7790 } else static assert(0); 7791 } 7792 7793 this(Widget parent) { 7794 super(parent); 7795 7796 version(win32_widgets) { 7797 createWin32Window(this, "Scrollbar"w, "", 7798 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 7799 } else version(custom_widgets) { 7800 auto vl = new VerticalLayout(this); 7801 auto upButton = new ArrowButton(ArrowDirection.up, vl); 7802 upButton.setClickRepeat(scrollClickRepeatInterval); 7803 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 7804 auto downButton = new ArrowButton(ArrowDirection.down, vl); 7805 downButton.setClickRepeat(scrollClickRepeatInterval); 7806 7807 upButton.addEventListener(EventType.triggered, () { 7808 this.emitCommand!"scrolltopreviousline"(); 7809 //informProgramThatUserChangedPosition(position - step()); 7810 }); 7811 downButton.addEventListener(EventType.triggered, () { 7812 this.emitCommand!"scrolltonextline"(); 7813 //informProgramThatUserChangedPosition(position + step()); 7814 }); 7815 7816 thumb.thumbWidth = this.minWidth; 7817 thumb.thumbHeight = scaleWithDpi(16); 7818 7819 thumb.addEventListener(EventType.change, () { 7820 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 7821 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 7822 7823 auto ev = new ScrollToPositionEvent(this, sy); 7824 ev.dispatch(); 7825 7826 //informProgramThatUserChangedPosition(sy); 7827 }); 7828 7829 upButton.tabStop = false; 7830 downButton.tabStop = false; 7831 thumb.tabStop = false; 7832 } 7833 } 7834 7835 version(custom_widgets) 7836 override void dpiChanged() { 7837 thumb.thumbWidth = scaleWithDpi(16); 7838 } 7839 7840 override int minWidth() { return scaleWithDpi(16); } 7841 override int maxWidth() { return scaleWithDpi(16); } 7842 override int minHeight() { return scaleWithDpi(48); } 7843 } 7844 7845 7846 /++ 7847 EXPERIMENTAL 7848 7849 A widget specialized for being a container for other widgets. 7850 7851 History: 7852 Added May 29, 2021. Not stabilized at this time. 7853 +/ 7854 class WidgetContainer : Widget { 7855 this(Widget parent) { 7856 tabStop = false; 7857 super(parent); 7858 } 7859 7860 override int maxHeight() { 7861 if(this.children.length == 1) { 7862 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 7863 } else { 7864 return int.max; 7865 } 7866 } 7867 7868 override int maxWidth() { 7869 if(this.children.length == 1) { 7870 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 7871 } else { 7872 return int.max; 7873 } 7874 } 7875 7876 /+ 7877 7878 override int minHeight() { 7879 int largest = 0; 7880 int margins = 0; 7881 int lastMargin = 0; 7882 foreach(child; children) { 7883 auto mh = child.minHeight(); 7884 if(mh > largest) 7885 largest = mh; 7886 margins += mymax(lastMargin, child.marginTop()); 7887 lastMargin = child.marginBottom(); 7888 } 7889 return largest + margins; 7890 } 7891 7892 override int maxHeight() { 7893 int largest = 0; 7894 int margins = 0; 7895 int lastMargin = 0; 7896 foreach(child; children) { 7897 auto mh = child.maxHeight(); 7898 if(mh == int.max) 7899 return int.max; 7900 if(mh > largest) 7901 largest = mh; 7902 margins += mymax(lastMargin, child.marginTop()); 7903 lastMargin = child.marginBottom(); 7904 } 7905 return largest + margins; 7906 } 7907 7908 override int minWidth() { 7909 int min; 7910 foreach(child; children) { 7911 auto cm = child.minWidth; 7912 if(cm > min) 7913 min = cm; 7914 } 7915 return min + paddingLeft + paddingRight; 7916 } 7917 7918 override int minHeight() { 7919 int min; 7920 foreach(child; children) { 7921 auto cm = child.minHeight; 7922 if(cm > min) 7923 min = cm; 7924 } 7925 return min + paddingTop + paddingBottom; 7926 } 7927 7928 override int maxHeight() { 7929 int largest = 0; 7930 int margins = 0; 7931 int lastMargin = 0; 7932 foreach(child; children) { 7933 auto mh = child.maxHeight(); 7934 if(mh == int.max) 7935 return int.max; 7936 if(mh > largest) 7937 largest = mh; 7938 margins += mymax(lastMargin, child.marginTop()); 7939 lastMargin = child.marginBottom(); 7940 } 7941 return largest + margins; 7942 } 7943 7944 override int heightStretchiness() { 7945 int max; 7946 foreach(child; children) { 7947 auto c = child.heightStretchiness; 7948 if(c > max) 7949 max = c; 7950 } 7951 return max; 7952 } 7953 7954 override int marginTop() { 7955 if(this.children.length) 7956 return this.children[0].marginTop; 7957 return 0; 7958 } 7959 +/ 7960 } 7961 7962 /// 7963 abstract class Layout : Widget { 7964 this(Widget parent) { 7965 tabStop = false; 7966 super(parent); 7967 } 7968 } 7969 7970 /++ 7971 Makes all children minimum width and height, placing them down 7972 left to right, top to bottom. 7973 7974 Useful if you want to make a list of buttons that automatically 7975 wrap to a new line when necessary. 7976 +/ 7977 class InlineBlockLayout : Layout { 7978 /// 7979 this(Widget parent) { super(parent); } 7980 7981 override void recomputeChildLayout() { 7982 registerMovement(); 7983 7984 int x = this.paddingLeft, y = this.paddingTop; 7985 7986 int lineHeight; 7987 int previousMargin = 0; 7988 int previousMarginBottom = 0; 7989 7990 foreach(child; children) { 7991 if(child.hidden) 7992 continue; 7993 if(cast(FixedPosition) child) { 7994 child.recomputeChildLayout(); 7995 continue; 7996 } 7997 child.width = child.flexBasisWidth(); 7998 if(child.width == 0) 7999 child.width = child.minWidth(); 8000 if(child.width == 0) 8001 child.width = 32; 8002 8003 child.height = child.flexBasisHeight(); 8004 if(child.height == 0) 8005 child.height = child.minHeight(); 8006 if(child.height == 0) 8007 child.height = 32; 8008 8009 if(x + child.width + paddingRight > this.width) { 8010 x = this.paddingLeft; 8011 y += lineHeight; 8012 lineHeight = 0; 8013 previousMargin = 0; 8014 previousMarginBottom = 0; 8015 } 8016 8017 auto margin = child.marginLeft; 8018 if(previousMargin > margin) 8019 margin = previousMargin; 8020 8021 x += margin; 8022 8023 child.x = x; 8024 child.y = y; 8025 8026 int marginTopApplied; 8027 if(child.marginTop > previousMarginBottom) { 8028 child.y += child.marginTop; 8029 marginTopApplied = child.marginTop; 8030 } 8031 8032 x += child.width; 8033 previousMargin = child.marginRight; 8034 8035 if(child.marginBottom > previousMarginBottom) 8036 previousMarginBottom = child.marginBottom; 8037 8038 auto h = child.height + previousMarginBottom + marginTopApplied; 8039 if(h > lineHeight) 8040 lineHeight = h; 8041 8042 child.recomputeChildLayout(); 8043 } 8044 8045 } 8046 8047 override int minWidth() { 8048 int min; 8049 foreach(child; children) { 8050 auto cm = child.minWidth; 8051 if(cm > min) 8052 min = cm; 8053 } 8054 return min + paddingLeft + paddingRight; 8055 } 8056 8057 override int minHeight() { 8058 int min; 8059 foreach(child; children) { 8060 auto cm = child.minHeight; 8061 if(cm > min) 8062 min = cm; 8063 } 8064 return min + paddingTop + paddingBottom; 8065 } 8066 } 8067 8068 /++ 8069 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 8070 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 8071 the [TabWidget] will automatically change pages of child widgets. 8072 8073 This allows you to react to it however you see fit rather than having to 8074 be tied to just the new sets of child widgets. 8075 8076 It sends the message in the form of `this.emitCommand!"changetab"();`. 8077 8078 History: 8079 Added December 24, 2021 (dub v10.5) 8080 +/ 8081 class TabMessageWidget : Widget { 8082 8083 protected void tabIndexClicked(int item) { 8084 this.emitCommand!"changetab"(); 8085 } 8086 8087 /++ 8088 Adds the a new tab to the control with the given title. 8089 8090 Returns: 8091 The index of the newly added tab. You will need to know 8092 this index to refer to it later and to know which tab to 8093 change to when you get a changetab message. 8094 +/ 8095 int addTab(string title, int pos = int.max) { 8096 version(win32_widgets) { 8097 TCITEM item; 8098 item.mask = TCIF_TEXT; 8099 WCharzBuffer buf = WCharzBuffer(title); 8100 item.pszText = buf.ptr; 8101 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 8102 } else version(custom_widgets) { 8103 if(pos >= tabs.length) { 8104 tabs ~= title; 8105 redraw(); 8106 return cast(int) tabs.length - 1; 8107 } else if(pos <= 0) { 8108 tabs = title ~ tabs; 8109 redraw(); 8110 return 0; 8111 } else { 8112 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 8113 redraw(); 8114 return pos; 8115 } 8116 } 8117 } 8118 8119 override void addChild(Widget child, int pos = int.max) { 8120 if(container) 8121 container.addChild(child, pos); 8122 else 8123 super.addChild(child, pos); 8124 } 8125 8126 protected Widget makeContainer() { 8127 return new Widget(this); 8128 } 8129 8130 private Widget container; 8131 8132 override void recomputeChildLayout() { 8133 version(win32_widgets) { 8134 this.registerMovement(); 8135 8136 RECT rect; 8137 GetWindowRect(hwnd, &rect); 8138 8139 auto left = rect.left; 8140 auto top = rect.top; 8141 8142 TabCtrl_AdjustRect(hwnd, false, &rect); 8143 foreach(child; children) { 8144 if(!child.showing) continue; 8145 child.x = rect.left - left; 8146 child.y = rect.top - top; 8147 child.width = rect.right - rect.left; 8148 child.height = rect.bottom - rect.top; 8149 child.recomputeChildLayout(); 8150 } 8151 } else version(custom_widgets) { 8152 this.registerMovement(); 8153 foreach(child; children) { 8154 if(!child.showing) continue; 8155 child.x = 2; 8156 child.y = tabBarHeight + 2; // for the border 8157 child.width = width - 4; // for the border 8158 child.height = height - tabBarHeight - 2 - 2; // for the border 8159 child.recomputeChildLayout(); 8160 } 8161 } else static assert(0); 8162 } 8163 8164 this(Widget parent) { 8165 super(parent); 8166 8167 tabStop = false; 8168 8169 version(win32_widgets) { 8170 createWin32Window(this, WC_TABCONTROL, "", 0); 8171 } else version(custom_widgets) { 8172 addEventListener((ClickEvent event) { 8173 if(event.target !is this) 8174 return; 8175 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 8176 auto t = (event.clientX / tabWidth); 8177 if(t >= 0 && t < tabs.length) { 8178 currentTab_ = t; 8179 tabIndexClicked(t); 8180 redraw(); 8181 } 8182 } 8183 }); 8184 } else static assert(0); 8185 8186 this.container = makeContainer(); 8187 } 8188 8189 override int marginTop() { return 4; } 8190 override int paddingBottom() { return 4; } 8191 8192 override int minHeight() { 8193 int max = 0; 8194 foreach(child; children) 8195 max = mymax(child.minHeight, max); 8196 8197 8198 version(win32_widgets) { 8199 RECT rect; 8200 rect.right = this.width; 8201 rect.bottom = max; 8202 TabCtrl_AdjustRect(hwnd, true, &rect); 8203 8204 max = rect.bottom; 8205 } else { 8206 max += defaultLineHeight + 4; 8207 } 8208 8209 8210 return max; 8211 } 8212 8213 version(win32_widgets) 8214 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 8215 switch(code) { 8216 case TCN_SELCHANGE: 8217 auto sel = TabCtrl_GetCurSel(hwnd); 8218 tabIndexClicked(sel); 8219 break; 8220 default: 8221 } 8222 return 0; 8223 } 8224 8225 version(custom_widgets) { 8226 private int currentTab_; 8227 private int tabBarHeight() { return defaultLineHeight; } 8228 int tabWidth() { return scaleWithDpi(80); } 8229 8230 string[] tabs; 8231 } 8232 8233 version(win32_widgets) 8234 override void paint(WidgetPainter painter) {} 8235 8236 version(custom_widgets) 8237 override void paint(WidgetPainter painter) { 8238 auto cs = getComputedStyle(); 8239 8240 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 8241 8242 int posX = 0; 8243 foreach(idx, title; tabs) { 8244 auto isCurrent = idx == getCurrentTab(); 8245 8246 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 8247 8248 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 8249 painter.outlineColor = cs.foregroundColor; 8250 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 8251 8252 if(isCurrent) { 8253 painter.outlineColor = cs.windowBackgroundColor; 8254 painter.fillColor = Color.transparent; 8255 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 8256 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 8257 8258 painter.outlineColor = Color.white; 8259 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 8260 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 8261 painter.outlineColor = cs.activeTabColor; 8262 painter.drawPixel(Point(posX, tabBarHeight - 1)); 8263 } 8264 8265 posX += tabWidth - 2; 8266 } 8267 } 8268 8269 /// 8270 @scriptable 8271 void setCurrentTab(int item) { 8272 version(win32_widgets) 8273 TabCtrl_SetCurSel(hwnd, item); 8274 else version(custom_widgets) 8275 currentTab_ = item; 8276 else static assert(0); 8277 8278 tabIndexClicked(item); 8279 } 8280 8281 /// 8282 @scriptable 8283 int getCurrentTab() { 8284 version(win32_widgets) 8285 return TabCtrl_GetCurSel(hwnd); 8286 else version(custom_widgets) 8287 return currentTab_; // FIXME 8288 else static assert(0); 8289 } 8290 8291 /// 8292 @scriptable 8293 void removeTab(int item) { 8294 if(item && item == getCurrentTab()) 8295 setCurrentTab(item - 1); 8296 8297 version(win32_widgets) { 8298 TabCtrl_DeleteItem(hwnd, item); 8299 } 8300 8301 for(int a = item; a < children.length - 1; a++) 8302 this._children[a] = this._children[a + 1]; 8303 this._children = this._children[0 .. $-1]; 8304 } 8305 8306 } 8307 8308 8309 /++ 8310 A tab widget is a set of clickable tab buttons followed by a content area. 8311 8312 8313 Tabs can change existing content or can be new pages. 8314 8315 When the user picks a different tab, a `change` message is generated. 8316 +/ 8317 class TabWidget : TabMessageWidget { 8318 this(Widget parent) { 8319 super(parent); 8320 } 8321 8322 override protected Widget makeContainer() { 8323 return null; 8324 } 8325 8326 override void addChild(Widget child, int pos = int.max) { 8327 if(auto twp = cast(TabWidgetPage) child) { 8328 Widget.addChild(child, pos); 8329 if(pos == int.max) 8330 pos = cast(int) this.children.length - 1; 8331 8332 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 8333 8334 if(pos != getCurrentTab) { 8335 child.showing = false; 8336 } 8337 } else { 8338 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 8339 } 8340 } 8341 8342 // FIXME: add tab icons at some point, Windows supports them 8343 /++ 8344 Adds a page and its associated tab with the given label to the widget. 8345 8346 Returns: 8347 The added page object, to which you can add other widgets. 8348 +/ 8349 @scriptable 8350 TabWidgetPage addPage(string title) { 8351 return new TabWidgetPage(title, this); 8352 } 8353 8354 /++ 8355 Gets the page at the given tab index, or `null` if the index is bad. 8356 8357 History: 8358 Added December 24, 2021. 8359 +/ 8360 TabWidgetPage getPage(int index) { 8361 if(index < this.children.length) 8362 return null; 8363 return cast(TabWidgetPage) this.children[index]; 8364 } 8365 8366 /++ 8367 While you can still use the addTab from the parent class, 8368 *strongly* recommend you use [addPage] insteaad. 8369 8370 History: 8371 Added December 24, 2021 to fulful the interface 8372 requirement that came from adding [TabMessageWidget]. 8373 8374 You should not use it though since the [addPage] function 8375 is much easier to use here. 8376 +/ 8377 override int addTab(string title, int pos = int.max) { 8378 auto p = addPage(title); 8379 foreach(idx, child; this.children) 8380 if(child is p) 8381 return cast(int) idx; 8382 return -1; 8383 } 8384 8385 protected override void tabIndexClicked(int item) { 8386 super.tabIndexClicked(item); 8387 foreach(idx, child; children) { 8388 child.showing(false, false); // batch the recalculates for the end 8389 } 8390 8391 foreach(idx, child; children) { 8392 if(idx == item) { 8393 child.showing(true, false); 8394 if(parentWindow) { 8395 auto f = parentWindow.getFirstFocusable(child); 8396 if(f) 8397 f.focus(); 8398 } 8399 recomputeChildLayout(); 8400 } 8401 } 8402 8403 version(win32_widgets) { 8404 InvalidateRect(hwnd, null, true); 8405 } else version(custom_widgets) { 8406 this.redraw(); 8407 } 8408 } 8409 8410 } 8411 8412 /++ 8413 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 8414 8415 You add [TabWidgetPage]s to it. 8416 +/ 8417 class PageWidget : Widget { 8418 this(Widget parent) { 8419 super(parent); 8420 } 8421 8422 override int minHeight() { 8423 int max = 0; 8424 foreach(child; children) 8425 max = mymax(child.minHeight, max); 8426 8427 return max; 8428 } 8429 8430 8431 override void addChild(Widget child, int pos = int.max) { 8432 if(auto twp = cast(TabWidgetPage) child) { 8433 super.addChild(child, pos); 8434 if(pos == int.max) 8435 pos = cast(int) this.children.length - 1; 8436 8437 if(pos != getCurrentTab) { 8438 child.showing = false; 8439 } 8440 } else { 8441 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 8442 } 8443 } 8444 8445 override void recomputeChildLayout() { 8446 this.registerMovement(); 8447 foreach(child; children) { 8448 child.x = 0; 8449 child.y = 0; 8450 child.width = width; 8451 child.height = height; 8452 child.recomputeChildLayout(); 8453 } 8454 } 8455 8456 private int currentTab_; 8457 8458 /// 8459 @scriptable 8460 void setCurrentTab(int item) { 8461 currentTab_ = item; 8462 8463 showOnly(item); 8464 } 8465 8466 /// 8467 @scriptable 8468 int getCurrentTab() { 8469 return currentTab_; 8470 } 8471 8472 /// 8473 @scriptable 8474 void removeTab(int item) { 8475 if(item && item == getCurrentTab()) 8476 setCurrentTab(item - 1); 8477 8478 for(int a = item; a < children.length - 1; a++) 8479 this._children[a] = this._children[a + 1]; 8480 this._children = this._children[0 .. $-1]; 8481 } 8482 8483 /// 8484 @scriptable 8485 TabWidgetPage addPage(string title) { 8486 return new TabWidgetPage(title, this); 8487 } 8488 8489 private void showOnly(int item) { 8490 foreach(idx, child; children) 8491 if(idx == item) { 8492 child.show(); 8493 child.queueRecomputeChildLayout(); 8494 } else { 8495 child.hide(); 8496 } 8497 } 8498 } 8499 8500 /++ 8501 8502 +/ 8503 class TabWidgetPage : Widget { 8504 this(string title, Widget parent) { 8505 this.title_ = title; 8506 this.tabStop = false; 8507 super(parent); 8508 8509 ///* 8510 version(win32_widgets) { 8511 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 8512 } 8513 //*/ 8514 } 8515 8516 private string title_; 8517 8518 /++ 8519 History: 8520 Prior to April 6, 2025, it was a public field. It was changed to properties so it can queue redraws; 8521 +/ 8522 string title() { 8523 return title_; 8524 } 8525 8526 /// ditto 8527 void title(string t) { 8528 title_ = t; 8529 version(custom_widgets) { 8530 if(auto tw = cast(TabWidget) parent) { 8531 foreach(idx, child; tw.children) 8532 if(child is this) 8533 tw.tabs[idx] = t; 8534 tw.redraw(); 8535 } 8536 } 8537 } 8538 8539 override int minHeight() { 8540 int sum = 0; 8541 foreach(child; children) 8542 sum += child.minHeight(); 8543 return sum; 8544 } 8545 } 8546 8547 version(none) 8548 /++ 8549 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 8550 8551 I think I need to modify the layout algorithms to support this. 8552 +/ 8553 class CollapsableSidebar : Widget { 8554 8555 } 8556 8557 /// Stacks the widgets vertically, taking all the available width for each child. 8558 class VerticalLayout : Layout { 8559 // most of this is intentionally blank - widget's default is vertical layout right now 8560 /// 8561 this(Widget parent) { super(parent); } 8562 8563 /++ 8564 Sets a max width for the layout so you don't have to subclass. The max width 8565 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8566 8567 History: 8568 Added November 29, 2021 (dub v10.5) 8569 +/ 8570 this(int maxWidth, Widget parent) { 8571 this.mw = maxWidth; 8572 super(parent); 8573 } 8574 8575 private int mw = int.max; 8576 8577 override int maxWidth() { return scaleWithDpi(mw); } 8578 } 8579 8580 /// Stacks the widgets horizontally, taking all the available height for each child. 8581 class HorizontalLayout : Layout { 8582 /// 8583 this(Widget parent) { super(parent); } 8584 8585 /++ 8586 Sets a max height for the layout so you don't have to subclass. The max height 8587 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8588 8589 History: 8590 Added November 29, 2021 (dub v10.5) 8591 +/ 8592 this(int maxHeight, Widget parent) { 8593 this.mh = maxHeight; 8594 super(parent); 8595 } 8596 8597 private int mh = 0; 8598 8599 8600 8601 override void recomputeChildLayout() { 8602 .recomputeChildLayout!"width"(this); 8603 } 8604 8605 override int minHeight() { 8606 int largest = 0; 8607 int margins = 0; 8608 int lastMargin = 0; 8609 foreach(child; children) { 8610 auto mh = child.minHeight(); 8611 if(mh > largest) 8612 largest = mh; 8613 margins += mymax(lastMargin, child.marginTop()); 8614 lastMargin = child.marginBottom(); 8615 } 8616 return largest + margins; 8617 } 8618 8619 override int maxHeight() { 8620 if(mh != 0) 8621 return mymax(minHeight, scaleWithDpi(mh)); 8622 8623 int largest = 0; 8624 int margins = 0; 8625 int lastMargin = 0; 8626 foreach(child; children) { 8627 auto mh = child.maxHeight(); 8628 if(mh == int.max) 8629 return int.max; 8630 if(mh > largest) 8631 largest = mh; 8632 margins += mymax(lastMargin, child.marginTop()); 8633 lastMargin = child.marginBottom(); 8634 } 8635 return largest + margins; 8636 } 8637 8638 override int heightStretchiness() { 8639 int max; 8640 foreach(child; children) { 8641 auto c = child.heightStretchiness; 8642 if(c > max) 8643 max = c; 8644 } 8645 return max; 8646 } 8647 } 8648 8649 version(win32_widgets) 8650 private 8651 extern(Windows) 8652 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 8653 Widget* pwin = hwnd in Widget.nativeMapping; 8654 if(pwin is null) 8655 return DefWindowProc(hwnd, message, wparam, lparam); 8656 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 8657 if(win is null) 8658 return DefWindowProc(hwnd, message, wparam, lparam); 8659 8660 switch(message) { 8661 case WM_SIZE: 8662 auto width = LOWORD(lparam); 8663 auto height = HIWORD(lparam); 8664 8665 auto hdc = GetDC(hwnd); 8666 auto hdcBmp = CreateCompatibleDC(hdc); 8667 8668 // FIXME: could this be more efficient? it never relinquishes a large bitmap 8669 if(width > win.bmpWidth || height > win.bmpHeight) { 8670 auto oldBuffer = win.buffer; 8671 win.buffer = CreateCompatibleBitmap(hdc, width, height); 8672 8673 if(oldBuffer) 8674 DeleteObject(oldBuffer); 8675 8676 win.bmpWidth = width; 8677 win.bmpHeight = height; 8678 } 8679 8680 // just always erase it upon resizing so minigui can draw over with a clean slate 8681 auto oldBmp = SelectObject(hdcBmp, win.buffer); 8682 8683 auto brush = GetSysColorBrush(COLOR_3DFACE); 8684 RECT r; 8685 r.left = 0; 8686 r.top = 0; 8687 r.right = width; 8688 r.bottom = height; 8689 FillRect(hdcBmp, &r, brush); 8690 8691 SelectObject(hdcBmp, oldBmp); 8692 DeleteDC(hdcBmp); 8693 ReleaseDC(hwnd, hdc); 8694 break; 8695 case WM_PAINT: 8696 if(win.buffer is null) 8697 goto default; 8698 8699 BITMAP bm; 8700 PAINTSTRUCT ps; 8701 8702 HDC hdc = BeginPaint(hwnd, &ps); 8703 8704 HDC hdcMem = CreateCompatibleDC(hdc); 8705 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 8706 8707 GetObject(win.buffer, bm.sizeof, &bm); 8708 8709 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 8710 8711 SelectObject(hdcMem, hbmOld); 8712 DeleteDC(hdcMem); 8713 EndPaint(hwnd, &ps); 8714 break; 8715 default: 8716 return DefWindowProc(hwnd, message, wparam, lparam); 8717 } 8718 8719 return 0; 8720 } 8721 8722 private wstring Win32Class(wstring name)() { 8723 static bool classRegistered; 8724 if(!classRegistered) { 8725 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 8726 WNDCLASSEX wc; 8727 wc.cbSize = wc.sizeof; 8728 wc.hInstance = hInstance; 8729 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 8730 wc.lpfnWndProc = &DoubleBufferWndProc; 8731 wc.lpszClassName = name.ptr; 8732 if(!RegisterClassExW(&wc)) 8733 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 8734 classRegistered = true; 8735 } 8736 8737 return name; 8738 } 8739 8740 /+ 8741 version(win32_widgets) 8742 extern(Windows) 8743 private 8744 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 8745 switch(iMessage) { 8746 case WM_PAINT: 8747 if(auto te = hWnd in Widget.nativeMapping) { 8748 try { 8749 //te.redraw(); 8750 writeln(te, " drawing"); 8751 } catch(Exception) {} 8752 } 8753 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8754 default: 8755 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8756 } 8757 } 8758 +/ 8759 8760 8761 /++ 8762 A widget specifically designed to hold other widgets. 8763 8764 History: 8765 Added July 1, 2021 8766 +/ 8767 class ContainerWidget : Widget { 8768 this(Widget parent) { 8769 super(parent); 8770 this.tabStop = false; 8771 8772 version(win32_widgets) { 8773 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 8774 } 8775 } 8776 } 8777 8778 /++ 8779 A widget that takes your widget, puts scroll bars around it, and sends 8780 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 8781 no effort to automatically scroll or clip its child widgets - it just sends 8782 the messages. 8783 8784 8785 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 8786 The scroll coordinates are all given in a unit you interpret as you wish. One 8787 of these units is moved on each press of the arrow buttons and represents the 8788 smallest amount the user can scroll. The intention is for this to be one line, 8789 one item in a list, one row in a table, etc. Whatever makes sense for your widget 8790 in each direction that the user might be interested in. 8791 8792 You can set a "page size" with the [step] property. (Yes, I regret the name...) 8793 This is the amount it jumps when the user pressed page up and page down, or clicks 8794 in the exposed part of the scroll bar. 8795 8796 You should add child content to the ScrollMessageWidget. However, it is important to 8797 note that the coordinates are always independent of the scroll position! It is YOUR 8798 responsibility to do any necessary transforms, clipping, etc., while drawing the 8799 content and interpreting mouse events if they are supposed to change with the scroll. 8800 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 8801 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 8802 you more control (which can be considerably more efficient and adapted to your actual data) 8803 at the expense of you also needing to be aware of its reality. 8804 8805 Please note that it does NOT react to mouse wheel events or various keyboard events as of 8806 version 10.3. Maybe this will change in the future.... but for now you must call 8807 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 8808 +/ 8809 class ScrollMessageWidget : Widget { 8810 this(Widget parent) { 8811 super(parent); 8812 8813 container = new Widget(this); 8814 hsb = new HorizontalScrollbar(this); 8815 vsb = new VerticalScrollbar(this); 8816 8817 hsb.addEventListener("scrolltonextline", { 8818 hsb.setPosition(hsb.position + movementPerButtonClickH_); 8819 notify(); 8820 }); 8821 hsb.addEventListener("scrolltopreviousline", { 8822 hsb.setPosition(hsb.position - movementPerButtonClickH_); 8823 notify(); 8824 }); 8825 vsb.addEventListener("scrolltonextline", { 8826 vsb.setPosition(vsb.position + movementPerButtonClickV_); 8827 notify(); 8828 }); 8829 vsb.addEventListener("scrolltopreviousline", { 8830 vsb.setPosition(vsb.position - movementPerButtonClickV_); 8831 notify(); 8832 }); 8833 hsb.addEventListener("scrolltonextpage", { 8834 hsb.setPosition(hsb.position + hsb.step_); 8835 notify(); 8836 }); 8837 hsb.addEventListener("scrolltopreviouspage", { 8838 hsb.setPosition(hsb.position - hsb.step_); 8839 notify(); 8840 }); 8841 vsb.addEventListener("scrolltonextpage", { 8842 vsb.setPosition(vsb.position + vsb.step_); 8843 notify(); 8844 }); 8845 vsb.addEventListener("scrolltopreviouspage", { 8846 vsb.setPosition(vsb.position - vsb.step_); 8847 notify(); 8848 }); 8849 hsb.addEventListener("scrolltoposition", (Event event) { 8850 hsb.setPosition(event.intValue); 8851 notify(); 8852 }); 8853 vsb.addEventListener("scrolltoposition", (Event event) { 8854 vsb.setPosition(event.intValue); 8855 notify(); 8856 }); 8857 8858 8859 tabStop = false; 8860 container.tabStop = false; 8861 magic = true; 8862 } 8863 8864 private int movementPerButtonClickH_ = 1; 8865 private int movementPerButtonClickV_ = 1; 8866 public void movementPerButtonClick(int h, int v) { 8867 movementPerButtonClickH_ = h; 8868 movementPerButtonClickV_ = v; 8869 } 8870 8871 /++ 8872 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 8873 8874 8875 The defaults for [addDefaultWheelListeners] are: 8876 8877 $(LIST 8878 * Mouse wheel scrolls vertically 8879 * Alt key + mouse wheel scrolls horiontally 8880 * Shift + mouse wheel scrolls faster. 8881 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 8882 ) 8883 8884 The defaults for [addDefaultKeyboardListeners] are: 8885 8886 $(LIST 8887 * Arrow keys scroll by the given amounts 8888 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 8889 * Page up and down scroll by the vertical viewable area 8890 * Home and end scroll to the start and end of the verticle viewable area. 8891 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 8892 ) 8893 8894 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 8895 8896 Params: 8897 horizontalArrowScrollAmount = 8898 verticalArrowScrollAmount = 8899 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 8900 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 8901 shiftMultiplier = multiplies the scroll amount by this when shift is held 8902 +/ 8903 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 8904 defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount; 8905 defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount; 8906 defaultKeyboardListener_shiftMultiplier = shiftMultiplier; 8907 8908 container.addEventListener(&defaultKeyboardListener); 8909 } 8910 8911 /// ditto 8912 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 8913 auto _this = this; 8914 container.addEventListener((scope ClickEvent ce) { 8915 8916 //if(ce.target && ce.target.tabStop) 8917 //ce.target.focus(); 8918 8919 // ctrl is reserved for the application 8920 if(ce.ctrlKey) 8921 return; 8922 8923 if(horizontalWheelScrollAmount == 0 && ce.altKey) 8924 return; 8925 8926 if(shiftMultiplier == 0 && ce.shiftKey) 8927 return; 8928 8929 if(ce.button == MouseButton.wheelDown) { 8930 if(ce.altKey) 8931 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8932 else 8933 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8934 } else if(ce.button == MouseButton.wheelUp) { 8935 if(ce.altKey) 8936 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8937 else 8938 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8939 } 8940 else 8941 if(ce.button == MouseButton.wheelRight) { 8942 if(ce.altKey) 8943 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8944 else 8945 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8946 } else if(ce.button == MouseButton.wheelLeft) { 8947 if(ce.altKey) 8948 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8949 else 8950 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8951 } 8952 8953 }); 8954 } 8955 8956 int defaultKeyboardListener_verticalArrowScrollAmount = 1; 8957 int defaultKeyboardListener_horizontalArrowScrollAmount = 1; 8958 int defaultKeyboardListener_shiftMultiplier = 3; 8959 8960 void defaultKeyboardListener(scope KeyDownEvent ke) { 8961 switch(ke.key) { 8962 case Key.Left: 8963 this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8964 break; 8965 case Key.Right: 8966 this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8967 break; 8968 case Key.Up: 8969 this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8970 break; 8971 case Key.Down: 8972 this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8973 break; 8974 case Key.PageUp: 8975 if(ke.altKey) 8976 this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8977 else 8978 this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8979 break; 8980 case Key.PageDown: 8981 if(ke.altKey) 8982 this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8983 else 8984 this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8985 break; 8986 case Key.Home: 8987 if(ke.altKey) 8988 this.scrollLeft(short.max * 16); 8989 else 8990 this.scrollUp(short.max * 16); 8991 break; 8992 case Key.End: 8993 if(ke.altKey) 8994 this.scrollRight(short.max * 16); 8995 else 8996 this.scrollDown(short.max * 16); 8997 break; 8998 8999 default: 9000 // ignore, not for us. 9001 } 9002 } 9003 9004 /++ 9005 Scrolls the given amount. 9006 9007 History: 9008 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. 9009 +/ 9010 void scrollUp(int amount = 1) { 9011 vsb.setPosition(vsb.position.NonOverflowingInt - amount); 9012 notify(); 9013 } 9014 /// ditto 9015 void scrollDown(int amount = 1) { 9016 vsb.setPosition(vsb.position.NonOverflowingInt + amount); 9017 notify(); 9018 } 9019 /// ditto 9020 void scrollLeft(int amount = 1) { 9021 hsb.setPosition(hsb.position.NonOverflowingInt - amount); 9022 notify(); 9023 } 9024 /// ditto 9025 void scrollRight(int amount = 1) { 9026 hsb.setPosition(hsb.position.NonOverflowingInt + amount); 9027 notify(); 9028 } 9029 9030 /// 9031 VerticalScrollbar verticalScrollBar() { return vsb; } 9032 /// 9033 HorizontalScrollbar horizontalScrollBar() { return hsb; } 9034 9035 void notify() { 9036 static bool insideNotify; 9037 9038 if(insideNotify) 9039 return; // avoid the recursive call, even if it isn't strictly correct 9040 9041 insideNotify = true; 9042 scope(exit) insideNotify = false; 9043 9044 this.emit!ScrollEvent(); 9045 } 9046 9047 mixin Emits!ScrollEvent; 9048 9049 /// 9050 Point position() { 9051 return Point(hsb.position, vsb.position); 9052 } 9053 9054 /// 9055 void setPosition(int x, int y) { 9056 hsb.setPosition(x); 9057 vsb.setPosition(y); 9058 } 9059 9060 /// 9061 void setPageSize(int unitsX, int unitsY) { 9062 hsb.setStep(unitsX); 9063 vsb.setStep(unitsY); 9064 } 9065 9066 /// Always call this BEFORE setViewableArea 9067 void setTotalArea(int width, int height) { 9068 hsb.setMax(width); 9069 vsb.setMax(height); 9070 } 9071 9072 /++ 9073 Always set the viewable area AFTER setitng the total area if you are going to change both. 9074 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 9075 If you need to do that, use [queueRecomputeChildLayout]. 9076 +/ 9077 void setViewableArea(int width, int height) { 9078 9079 // actually there IS A need to dothis cuz the max might have changed since then 9080 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 9081 //return; // no need to do what is already done 9082 hsb.setViewableArea(width); 9083 vsb.setViewableArea(height); 9084 9085 bool needsNotify = false; 9086 9087 // FIXME: if at any point the rhs is outside the scrollbar, we need 9088 // to reset to 0. but it should remember the old position in case the 9089 // window resizes again, so it can kinda return ot where it was. 9090 // 9091 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 9092 if(width >= hsb.max) { 9093 // there's plenty of room to display it all so we need to reset to zero 9094 // FIXME: adjust so it matches the note above 9095 hsb.setPosition(0); 9096 needsNotify = true; 9097 } 9098 if(height >= vsb.max) { 9099 // there's plenty of room to display it all so we need to reset to zero 9100 // FIXME: adjust so it matches the note above 9101 vsb.setPosition(0); 9102 needsNotify = true; 9103 } 9104 if(needsNotify) 9105 notify(); 9106 } 9107 9108 private bool magic; 9109 override void addChild(Widget w, int position = int.max) { 9110 if(magic) 9111 container.addChild(w, position); 9112 else 9113 super.addChild(w, position); 9114 } 9115 9116 override void recomputeChildLayout() { 9117 if(hsb is null || vsb is null || container is null) return; 9118 9119 registerMovement(); 9120 9121 enum BUTTON_SIZE = 16; 9122 9123 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 9124 hsb.x = 0; 9125 hsb.y = this.height - hsb.height; 9126 9127 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 9128 vsb.x = this.width - vsb.width; 9129 vsb.y = 0; 9130 9131 auto vsb_width = vsb.showing ? vsb.width : 0; 9132 auto hsb_height = hsb.showing ? hsb.height : 0; 9133 9134 hsb.width = this.width - vsb_width; 9135 vsb.height = this.height - hsb_height; 9136 9137 hsb.recomputeChildLayout(); 9138 vsb.recomputeChildLayout(); 9139 9140 if(this.header is null) { 9141 container.x = 0; 9142 container.y = 0; 9143 container.width = this.width - vsb_width; 9144 container.height = this.height - hsb_height; 9145 container.recomputeChildLayout(); 9146 } else { 9147 header.x = 0; 9148 header.y = 0; 9149 header.width = this.width - vsb_width; 9150 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 9151 header.recomputeChildLayout(); 9152 9153 container.x = 0; 9154 container.y = scaleWithDpi(BUTTON_SIZE); 9155 container.width = this.width - vsb_width; 9156 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 9157 container.recomputeChildLayout(); 9158 } 9159 } 9160 9161 private HorizontalScrollbar hsb; 9162 private VerticalScrollbar vsb; 9163 Widget container; 9164 private Widget header; 9165 9166 /++ 9167 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 9168 9169 History: 9170 Added September 27, 2021 (dub v10.3) 9171 +/ 9172 Widget getHeader() { 9173 if(this.header is null) { 9174 magic = false; 9175 scope(exit) magic = true; 9176 this.header = new Widget(this); 9177 queueRecomputeChildLayout(); 9178 } 9179 return this.header; 9180 } 9181 9182 /++ 9183 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 9184 9185 History: 9186 Added January 3, 2023 (dub v11.0) 9187 +/ 9188 void scrollIntoView(Rectangle rect) { 9189 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 9190 9191 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 9192 9193 // the lower right is exclusive normally 9194 auto test = rect.lowerRight; 9195 if(test.x > 0) test.x--; 9196 if(test.y > 0) test.y--; 9197 9198 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 9199 // try to scroll only one dimension at a time if we can 9200 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 9201 setPosition(rect.upperLeft.x, position.y); 9202 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 9203 setPosition(position.x, rect.upperLeft.y); 9204 } 9205 9206 } 9207 9208 override int minHeight() { 9209 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 9210 if(header !is null) 9211 min += header.minHeight; 9212 if(horizontalScrollBar.showing) 9213 min += horizontalScrollBar.minHeight; 9214 return min; 9215 } 9216 9217 override int maxHeight() { 9218 int max = container ? container.maxHeight : int.max; 9219 if(max == int.max) 9220 return max; 9221 if(horizontalScrollBar.showing) 9222 max += horizontalScrollBar.minHeight; 9223 return max; 9224 } 9225 9226 static class Style : Widget.Style { 9227 override WidgetBackground background() { 9228 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 9229 } 9230 } 9231 mixin OverrideStyle!Style; 9232 } 9233 9234 /++ 9235 $(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") 9236 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 9237 +/ 9238 version(minigui_screenshots) 9239 @Screenshot("ScrollMessageWidget") 9240 unittest { 9241 auto window = new Window("ScrollMessageWidget"); 9242 9243 auto smw = new ScrollMessageWidget(window); 9244 smw.addDefaultKeyboardListeners(); 9245 smw.addDefaultWheelListeners(); 9246 9247 window.loop(); 9248 } 9249 9250 /++ 9251 Bypasses automatic layout for its children, using manual positioning and sizing only. 9252 While you need to manually position them, you must ensure they are inside the StaticLayout's 9253 bounding box to avoid undefined behavior. 9254 9255 You should almost never use this. 9256 +/ 9257 class StaticLayout : Layout { 9258 /// 9259 this(Widget parent) { super(parent); } 9260 override void recomputeChildLayout() { 9261 registerMovement(); 9262 foreach(child; children) 9263 child.recomputeChildLayout(); 9264 } 9265 } 9266 9267 /++ 9268 Bypasses automatic positioning when being laid out. It is your responsibility to make 9269 room for this widget in the parent layout. 9270 9271 Its children are laid out normally, unless there is exactly one, in which case it takes 9272 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 9273 can do that with `padding`). 9274 +/ 9275 class StaticPosition : Layout { 9276 /// 9277 this(Widget parent) { super(parent); } 9278 9279 override void recomputeChildLayout() { 9280 registerMovement(); 9281 if(this.children.length == 1) { 9282 auto child = children[0]; 9283 child.x = 0; 9284 child.y = 0; 9285 child.width = this.width; 9286 child.height = this.height; 9287 child.recomputeChildLayout(); 9288 } else 9289 foreach(child; children) 9290 child.recomputeChildLayout(); 9291 } 9292 9293 alias width = typeof(super).width; 9294 alias height = typeof(super).height; 9295 9296 @property int width(int w) @nogc pure @safe nothrow { 9297 return this._width = w; 9298 } 9299 9300 @property int height(int w) @nogc pure @safe nothrow { 9301 return this._height = w; 9302 } 9303 9304 } 9305 9306 /++ 9307 FixedPosition is like [StaticPosition], but its coordinates 9308 are always relative to the viewport, meaning they do not scroll with 9309 the parent content. 9310 +/ 9311 class FixedPosition : StaticPosition { 9312 /// 9313 this(Widget parent) { super(parent); } 9314 } 9315 9316 version(win32_widgets) 9317 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 9318 if(true) { 9319 // cmd == 0 = menu, cmd == 1 = accelerator 9320 if(auto item = idm in Action.mapping) { 9321 foreach(handler; (*item).triggered) 9322 handler(); 9323 /* 9324 auto event = new Event("triggered", *item); 9325 event.button = idm; 9326 event.dispatch(); 9327 */ 9328 return 0; 9329 } 9330 } 9331 if(handle) 9332 if(auto widgetp = handle in Widget.nativeMapping) { 9333 (*widgetp).handleWmCommand(cmd, idm); 9334 return 0; 9335 } 9336 return 1; 9337 } 9338 9339 9340 /// 9341 class Window : Widget { 9342 Widget[] mouseCapturedBy; 9343 void captureMouse(Widget byWhom) { 9344 assert(byWhom !is null); 9345 if(mouseCapturedBy.length > 0) { 9346 auto cc = mouseCapturedBy[$-1]; 9347 if(cc is byWhom) 9348 return; // or should it throw? 9349 auto par = byWhom; 9350 while(par) { 9351 if(cc is par) 9352 goto allowed; 9353 par = par.parent; 9354 } 9355 9356 throw new Exception("mouse is already captured by other widget"); 9357 } 9358 allowed: 9359 mouseCapturedBy ~= byWhom; 9360 if(mouseCapturedBy.length == 1) 9361 win.grabInput(false, true, false); 9362 //void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) { 9363 } 9364 void releaseMouseCapture() { 9365 if(mouseCapturedBy.length == 0) 9366 return; // or should it throw? 9367 mouseCapturedBy = mouseCapturedBy[0 .. $-1]; 9368 mouseCapturedBy.assumeSafeAppend(); 9369 if(mouseCapturedBy.length == 0) 9370 win.releaseInputGrab(); 9371 } 9372 9373 9374 /++ 9375 9376 +/ 9377 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 9378 return .messageBox(this, title, message, style, icon); 9379 } 9380 9381 /// ditto 9382 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 9383 return messageBox(null, message, style, icon); 9384 } 9385 9386 9387 /++ 9388 Sets the window icon which is often seen in title bars and taskbars. 9389 9390 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. 9391 9392 History: 9393 Added April 5, 2022 (dub v10.8) 9394 +/ 9395 @property void icon(MemoryImage icon) { 9396 if(win && icon) 9397 win.icon = icon; 9398 } 9399 9400 // 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 9401 // this does NOT change the icon on the window! That's what the other overload is for 9402 static @property .icon icon(GenericIcons i) { 9403 return .icon(i); 9404 } 9405 9406 /// 9407 @scriptable 9408 @property bool focused() { 9409 return win.focused; 9410 } 9411 9412 static class Style : Widget.Style { 9413 override WidgetBackground background() { 9414 version(custom_widgets) 9415 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 9416 else version(win32_widgets) 9417 return WidgetBackground(Color.transparent); 9418 else static assert(0); 9419 } 9420 } 9421 mixin OverrideStyle!Style; 9422 9423 /++ 9424 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. 9425 +/ 9426 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 9427 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 9428 } 9429 9430 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 9431 OperatingSystemFont font; 9432 if(auto vt = WidgetPainter.visualTheme) { 9433 font = vt.defaultFontCached(96); // FIXME 9434 } 9435 9436 if(font is null) { 9437 static int defaultHeightCache; 9438 if(defaultHeightCache == 0) { 9439 font = new OperatingSystemFont; 9440 font.loadDefault; 9441 defaultHeightCache = castFnumToCnum(font.height());// * 5 / 4; 9442 } 9443 return defaultHeightCache; 9444 } 9445 9446 return castFnumToCnum(font.height());// * 5 / 4; 9447 } 9448 9449 Widget focusedWidget; 9450 9451 private SimpleWindow win_; 9452 9453 @property { 9454 /++ 9455 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 9456 9457 History: 9458 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 9459 +/ 9460 public SimpleWindow win() { 9461 return win_; 9462 } 9463 /// 9464 protected void win(SimpleWindow w) { 9465 win_ = w; 9466 } 9467 } 9468 9469 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 9470 this(Widget p) { 9471 tabStop = false; 9472 super(p); 9473 } 9474 9475 private void actualRedraw() { 9476 if(recomputeChildLayoutRequired) 9477 recomputeChildLayoutEntry(); 9478 if(!showing) return; 9479 9480 assert(parentWindow !is null); 9481 9482 auto w = drawableWindow; 9483 if(w is null) 9484 w = parentWindow.win; 9485 9486 if(w.closed()) 9487 return; 9488 9489 auto ugh = this.parent; 9490 int lox, loy; 9491 while(ugh) { 9492 lox += ugh.x; 9493 loy += ugh.y; 9494 ugh = ugh.parent; 9495 } 9496 auto painter = w.draw(true); 9497 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 9498 } 9499 9500 9501 private bool skipNextChar = false; 9502 9503 /++ 9504 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 9505 9506 This constructor is intended primarily for internal use and may be changed to `protected` later. 9507 +/ 9508 this(SimpleWindow win) { 9509 9510 static if(UsingSimpledisplayX11) { 9511 win.discardAdditionalConnectionState = &discardXConnectionState; 9512 win.recreateAdditionalConnectionState = &recreateXConnectionState; 9513 } 9514 9515 tabStop = false; 9516 super(null); 9517 this.win = win; 9518 9519 win.addEventListener((Widget.RedrawEvent) { 9520 if(win.eventQueued!RecomputeEvent) { 9521 // writeln("skipping"); 9522 return; // let the recompute event do the actual redraw 9523 } 9524 this.actualRedraw(); 9525 }); 9526 9527 win.addEventListener((Widget.RecomputeEvent) { 9528 recomputeChildLayoutEntry(); 9529 if(win.eventQueued!RedrawEvent) 9530 return; // let the queued one do it 9531 else { 9532 // writeln("drawing"); 9533 this.actualRedraw(); // if not queued, it needs to be done now anyway 9534 } 9535 }); 9536 9537 this.width = win.width; 9538 this.height = win.height; 9539 this.parentWindow = this; 9540 9541 win.closeQuery = () { 9542 if(this.emit!ClosingEvent()) 9543 win.close(); 9544 }; 9545 win.onClosing = () { 9546 this.emit!ClosedEvent(); 9547 }; 9548 9549 win.windowResized = (int w, int h) { 9550 this.width = w; 9551 this.height = h; 9552 queueRecomputeChildLayout(); 9553 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 9554 //version(win32_widgets) 9555 //InvalidateRect(hwnd, null, true); 9556 redraw(); 9557 }; 9558 9559 win.onFocusChange = (bool getting) { 9560 // sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString); 9561 if(this.focusedWidget) { 9562 if(getting) { 9563 this.focusedWidget.emit!FocusEvent(); 9564 this.focusedWidget.emit!FocusInEvent(); 9565 } else { 9566 this.focusedWidget.emit!BlurEvent(); 9567 this.focusedWidget.emit!FocusOutEvent(); 9568 } 9569 } 9570 9571 if(getting) { 9572 this.emit!FocusEvent(); 9573 this.emit!FocusInEvent(); 9574 } else { 9575 this.emit!BlurEvent(); 9576 this.emit!FocusOutEvent(); 9577 } 9578 }; 9579 9580 win.onDpiChanged = { 9581 this.queueRecomputeChildLayout(); 9582 auto event = new DpiChangedEvent(this); 9583 event.sendDirectly(); 9584 9585 privateDpiChanged(); 9586 }; 9587 9588 win.setEventHandlers( 9589 (MouseEvent e) { 9590 dispatchMouseEvent(e); 9591 }, 9592 (KeyEvent e) { 9593 //writefln("%x %s", cast(uint) e.key, e.key); 9594 dispatchKeyEvent(e); 9595 }, 9596 (dchar e) { 9597 if(e == 13) e = 10; // hack? 9598 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9599 dispatchCharEvent(e); 9600 }, 9601 ); 9602 9603 addEventListener("char", (Widget, Event ev) { 9604 if(skipNextChar) { 9605 ev.preventDefault(); 9606 skipNextChar = false; 9607 } 9608 }); 9609 9610 version(win32_widgets) 9611 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 9612 if(hwnd !is this.win.impl.hwnd) 9613 return 1; // we don't care... pass it on 9614 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 9615 if(mustReturn) 9616 return ret; 9617 return 1; // pass it on 9618 }; 9619 9620 if(Window.newWindowCreated) 9621 Window.newWindowCreated(this); 9622 } 9623 9624 version(custom_widgets) 9625 override void defaultEventHandler_click(ClickEvent event) { 9626 if(!event.isMouseWheel()) { 9627 if(event.target && event.target.tabStop) 9628 event.target.focus(); 9629 } 9630 } 9631 9632 private static void delegate(Window) newWindowCreated; 9633 9634 version(win32_widgets) 9635 override void paint(WidgetPainter painter) { 9636 /* 9637 RECT rect; 9638 rect.right = this.width; 9639 rect.bottom = this.height; 9640 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 9641 */ 9642 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 9643 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 9644 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 9645 // since the pen is null, to fill the whole space, we need the +1 on both. 9646 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 9647 SelectObject(painter.impl.hdc, p); 9648 SelectObject(painter.impl.hdc, b); 9649 } 9650 version(custom_widgets) 9651 override void paint(WidgetPainter painter) { 9652 auto cs = getComputedStyle(); 9653 painter.fillColor = cs.windowBackgroundColor; 9654 painter.outlineColor = cs.windowBackgroundColor; 9655 painter.drawRectangle(Point(0, 0), this.width, this.height); 9656 } 9657 9658 9659 override void defaultEventHandler_keydown(KeyDownEvent event) { 9660 Widget _this = event.target; 9661 9662 if(event.key == Key.Tab) { 9663 /* Window tab ordering is a recursive thingy with each group */ 9664 9665 // FIXME inefficient 9666 Widget[] helper(Widget p) { 9667 if(p.hidden) 9668 return null; 9669 Widget[] childOrdering; 9670 9671 auto children = p.children.dup; 9672 9673 while(true) { 9674 // UIs should be generally small, so gonna brute force it a little 9675 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 9676 9677 Widget smallestTab; 9678 foreach(ref c; children) { 9679 if(c is null) continue; 9680 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 9681 smallestTab = c; 9682 c = null; 9683 } 9684 } 9685 if(smallestTab !is null) { 9686 if(smallestTab.tabStop && !smallestTab.hidden) 9687 childOrdering ~= smallestTab; 9688 if(!smallestTab.hidden) 9689 childOrdering ~= helper(smallestTab); 9690 } else 9691 break; 9692 9693 } 9694 9695 return childOrdering; 9696 } 9697 9698 Widget[] tabOrdering = helper(this); 9699 9700 Widget recipient; 9701 9702 if(tabOrdering.length) { 9703 bool seenThis = false; 9704 Widget previous; 9705 foreach(idx, child; tabOrdering) { 9706 if(child is focusedWidget) { 9707 9708 if(event.shiftKey) { 9709 if(idx == 0) 9710 recipient = tabOrdering[$-1]; 9711 else 9712 recipient = tabOrdering[idx - 1]; 9713 break; 9714 } 9715 9716 seenThis = true; 9717 if(idx + 1 == tabOrdering.length) { 9718 // we're at the end, either move to the next group 9719 // or start back over 9720 recipient = tabOrdering[0]; 9721 } 9722 continue; 9723 } 9724 if(seenThis) { 9725 recipient = child; 9726 break; 9727 } 9728 previous = child; 9729 } 9730 } 9731 9732 if(recipient !is null) { 9733 // writeln(typeid(recipient)); 9734 recipient.focus(); 9735 9736 skipNextChar = true; 9737 } 9738 } 9739 9740 debug if(event.key == Key.F12) { 9741 if(devTools) { 9742 devTools.close(); 9743 devTools = null; 9744 } else { 9745 devTools = new DevToolWindow(this); 9746 devTools.show(); 9747 } 9748 } 9749 } 9750 9751 debug DevToolWindow devTools; 9752 9753 9754 /++ 9755 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 9756 9757 History: 9758 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 9759 9760 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 9761 +/ 9762 this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9763 if(title is null) { 9764 import core.runtime; 9765 if(Runtime.args.length) 9766 title = Runtime.args[0]; 9767 } 9768 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent); 9769 9770 static if(UsingSimpledisplayX11) 9771 if(windowFlags & WindowFlags.managesChildWindowFocus) { 9772 ///+ 9773 // for input proxy 9774 auto display = XDisplayConnection.get; 9775 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 9776 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 9777 XMapWindow(display, inputProxy); 9778 // writefln("input proxy: 0x%0x", inputProxy); 9779 this.inputProxy = new SimpleWindow(inputProxy); 9780 9781 /+ 9782 this.inputProxy.onFocusChange = (bool getting) { 9783 sdpyPrintDebugString("input proxy focus change ", getting); 9784 }; 9785 +/ 9786 9787 XEvent lastEvent; 9788 this.inputProxy.handleNativeEvent = (XEvent ev) { 9789 lastEvent = ev; 9790 return 1; 9791 }; 9792 this.inputProxy.setEventHandlers( 9793 (MouseEvent e) { 9794 dispatchMouseEvent(e); 9795 }, 9796 (KeyEvent e) { 9797 //writefln("%x %s", cast(uint) e.key, e.key); 9798 if(dispatchKeyEvent(e)) { 9799 // FIXME: i should trap error 9800 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 9801 auto thing = nw.focusableWindow(); 9802 if(thing && thing.window) { 9803 lastEvent.xkey.window = thing.window; 9804 // writeln("sending event ", lastEvent.xkey); 9805 trapXErrors( { 9806 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 9807 }); 9808 } 9809 } 9810 } 9811 }, 9812 (dchar e) { 9813 if(e == 13) e = 10; // hack? 9814 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9815 dispatchCharEvent(e); 9816 }, 9817 ); 9818 9819 this.inputProxy.populateXic(); 9820 // done 9821 //+/ 9822 } 9823 9824 9825 9826 win.setRequestedInputFocus = &this.setRequestedInputFocus; 9827 9828 this(win); 9829 } 9830 9831 SimpleWindow inputProxy; 9832 9833 private SimpleWindow setRequestedInputFocus() { 9834 return inputProxy; 9835 } 9836 9837 /// ditto 9838 this(string title, int width = 500, int height = 500) { 9839 this(width, height, title); 9840 } 9841 9842 /// 9843 @property string title() { return parentWindow.win.title; } 9844 /// 9845 @property void title(string title) { parentWindow.win.title = title; } 9846 9847 /// 9848 @scriptable 9849 void close() { 9850 win.close(); 9851 // I synchronize here upon window closing to ensure all child windows 9852 // get updated too before the event loop. This avoids some random X errors. 9853 static if(UsingSimpledisplayX11) { 9854 runInGuiThread( { 9855 XSync(XDisplayConnection.get, false); 9856 }); 9857 } 9858 } 9859 9860 bool dispatchKeyEvent(KeyEvent ev) { 9861 auto wid = focusedWidget; 9862 if(wid is null) 9863 wid = this; 9864 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 9865 event.originalKeyEvent = ev; 9866 event.key = ev.key; 9867 event.state = ev.modifierState; 9868 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9869 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9870 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9871 event.dispatch(); 9872 9873 return !event.propagationStopped; 9874 } 9875 9876 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 9877 bool dispatchCharEvent(dchar ch) { 9878 if(focusedWidget) { 9879 auto event = new CharEvent(focusedWidget, ch); 9880 event.dispatch(); 9881 return !event.propagationStopped; 9882 } 9883 return true; 9884 } 9885 9886 Widget mouseLastOver; 9887 Widget mouseLastDownOn; 9888 bool lastWasDoubleClick; 9889 bool dispatchMouseEvent(MouseEvent ev) { 9890 auto eleR = widgetAtPoint(this, ev.x, ev.y); 9891 auto ele = eleR.widget; 9892 9893 auto captureEle = ele; 9894 9895 auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null; 9896 if(mouseCapturedBy !is null) { 9897 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 9898 captureEle = mouseCapturedBy; 9899 } 9900 9901 // a hack to get it relative to the widget. 9902 eleR.x = ev.x; 9903 eleR.y = ev.y; 9904 auto pain = captureEle; 9905 9906 auto vpx = eleR.x; 9907 auto vpy = eleR.y; 9908 9909 while(pain) { 9910 eleR.x -= pain.x; 9911 eleR.y -= pain.y; 9912 pain.addScrollPosition(eleR.x, eleR.y); 9913 9914 vpx -= pain.x; 9915 vpy -= pain.y; 9916 9917 pain = pain.parent; 9918 } 9919 9920 void populateMouseEventBase(MouseEventBase event) { 9921 event.button = ev.button; 9922 event.buttonLinear = ev.buttonLinear; 9923 event.state = ev.modifierState; 9924 event.clientX = eleR.x; 9925 event.clientY = eleR.y; 9926 9927 event.viewportX = vpx; 9928 event.viewportY = vpy; 9929 9930 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9931 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9932 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9933 } 9934 9935 if(ev.type == MouseEventType.buttonPressed) { 9936 { 9937 auto event = new MouseDownEvent(captureEle); 9938 populateMouseEventBase(event); 9939 event.dispatch(); 9940 } 9941 9942 if(!ev.isMouseWheel && mouseLastDownOn is ele && ev.doubleClick) { 9943 auto event = new DoubleClickEvent(captureEle); 9944 populateMouseEventBase(event); 9945 event.dispatch(); 9946 lastWasDoubleClick = ev.doubleClick; 9947 } else { 9948 lastWasDoubleClick = false; 9949 } 9950 9951 mouseLastDownOn = ele; 9952 } else if(ev.type == MouseEventType.buttonReleased) { 9953 { 9954 auto event = new MouseUpEvent(captureEle); 9955 populateMouseEventBase(event); 9956 event.dispatch(); 9957 } 9958 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 9959 auto event = new ClickEvent(captureEle); 9960 populateMouseEventBase(event); 9961 event.dispatch(); 9962 } 9963 } else if(ev.type == MouseEventType.motion) { 9964 // motion 9965 { 9966 auto event = new MouseMoveEvent(captureEle); 9967 populateMouseEventBase(event); // fills in button which is meaningless but meh 9968 event.dispatch(); 9969 } 9970 9971 if(mouseLastOver !is ele) { 9972 if(ele !is null) { 9973 if(!isAParentOf(ele, mouseLastOver)) { 9974 ele.setDynamicState(DynamicState.hover, true); 9975 auto event = new MouseEnterEvent(ele); 9976 event.relatedTarget = mouseLastOver; 9977 event.sendDirectly(); 9978 9979 ele.useStyleProperties((scope Widget.Style s) { 9980 ele.parentWindow.win.cursor = s.cursor; 9981 }); 9982 } 9983 } 9984 9985 if(mouseLastOver !is null) { 9986 if(!isAParentOf(mouseLastOver, ele)) { 9987 mouseLastOver.setDynamicState(DynamicState.hover, false); 9988 auto event = new MouseLeaveEvent(mouseLastOver); 9989 event.relatedTarget = ele; 9990 event.sendDirectly(); 9991 } 9992 } 9993 9994 if(ele !is null) { 9995 auto event = new MouseOverEvent(ele); 9996 event.relatedTarget = mouseLastOver; 9997 event.dispatch(); 9998 } 9999 10000 if(mouseLastOver !is null) { 10001 auto event = new MouseOutEvent(mouseLastOver); 10002 event.relatedTarget = ele; 10003 event.dispatch(); 10004 } 10005 10006 mouseLastOver = ele; 10007 } 10008 } 10009 10010 return true; // FIXME: the event default prevented? 10011 } 10012 10013 /++ 10014 Shows the window and runs the application event loop. 10015 10016 Blocks until this window is closed. 10017 10018 Bugs: 10019 10020 $(PITFALL 10021 You should always have one event loop live for your application. 10022 If you make two windows in sequence, the second call to loop (or 10023 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 10024 might fail: 10025 10026 --- 10027 // don't do this! 10028 auto window = new Window(); 10029 window.loop(); 10030 10031 // or new Window or new MainWindow, all the same 10032 auto window2 = new SimpleWindow(); 10033 window2.eventLoop(0); // problematic! might crash 10034 --- 10035 10036 simpledisplay's current implementation assumes that final cleanup is 10037 done when the event loop refcount reaches zero. So after the first 10038 eventLoop returns, when there isn't already another one active, it assumes 10039 the program will exit soon and cleans up. 10040 10041 This is arguably a bug that it doesn't reinitialize, and I'll probably change 10042 it eventually, but in the mean time, there's an easy solution: 10043 10044 --- 10045 // do this 10046 EventLoop mainEventLoop = EventLoop.get; // just add this line 10047 10048 auto window = new Window(); 10049 window.loop(); 10050 10051 // or any other type of Window etc. 10052 auto window2 = new Window(); 10053 window2.loop(); // perfectly fine since mainEventLoop still alive 10054 --- 10055 10056 By adding a top-level reference to the event loop, it ensures the final cleanup 10057 is not performed until it goes out of scope too, letting the individual window loops 10058 work without trouble despite the bug. 10059 ) 10060 10061 History: 10062 The [BlockingMode] parameter was added on December 8, 2021. 10063 The default behavior is to block until the application quits 10064 (so all windows have been closed), unless another minigui or 10065 simpledisplay event loop is already running, in which case it 10066 will block until this window closes specifically. 10067 +/ 10068 @scriptable 10069 void loop(BlockingMode bm = BlockingMode.automatic) { 10070 if(win.closed) 10071 return; // otherwise show will throw 10072 show(); 10073 win.eventLoopWithBlockingMode(bm, 0); 10074 } 10075 10076 private bool firstShow = true; 10077 10078 @scriptable 10079 override void show() { 10080 bool rd = false; 10081 if(firstShow) { 10082 firstShow = false; 10083 queueRecomputeChildLayout(); 10084 // unless the programmer already called focus on something, pick something ourselves 10085 auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus? 10086 if(f) 10087 f.focus(); 10088 redraw(); 10089 } 10090 win.show(); 10091 super.show(); 10092 } 10093 @scriptable 10094 override void hide() { 10095 win.hide(); 10096 super.hide(); 10097 } 10098 10099 static Widget getFirstFocusable(Widget start) { 10100 if(start is null) 10101 return null; 10102 10103 foreach(widget; &start.focusableWidgets) { 10104 return widget; 10105 } 10106 10107 return null; 10108 } 10109 10110 static Widget getLastFocusable(Widget start) { 10111 if(start is null) 10112 return null; 10113 10114 Widget last; 10115 foreach(widget; &start.focusableWidgets) { 10116 last = widget; 10117 } 10118 10119 return last; 10120 } 10121 10122 10123 mixin Emits!ClosingEvent; 10124 mixin Emits!ClosedEvent; 10125 } 10126 10127 /++ 10128 History: 10129 Added January 12, 2022 10130 10131 Made `final` on January 3, 2025 10132 +/ 10133 final class DpiChangedEvent : Event { 10134 enum EventString = "dpichanged"; 10135 10136 this(Widget target) { 10137 super(EventString, target); 10138 } 10139 } 10140 10141 debug private class DevToolWindow : Window { 10142 Window p; 10143 10144 TextEdit parentList; 10145 TextEdit logWindow; 10146 TextLabel clickX, clickY; 10147 10148 this(Window p) { 10149 this.p = p; 10150 super(400, 300, "Developer Toolbox"); 10151 10152 logWindow = new TextEdit(this); 10153 parentList = new TextEdit(this); 10154 10155 auto hl = new HorizontalLayout(this); 10156 clickX = new TextLabel("", TextAlignment.Right, hl); 10157 clickY = new TextLabel("", TextAlignment.Right, hl); 10158 10159 parentListeners ~= p.addEventListener("*", (Event ev) { 10160 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 10161 }); 10162 10163 parentListeners ~= p.addEventListener((ClickEvent ev) { 10164 auto s = ev.srcElement; 10165 10166 string list; 10167 10168 void addInfo(Widget s) { 10169 list ~= s.toString(); 10170 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 10171 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 10172 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 10173 list ~= "\n\theight: " ~ toInternal!string(s.height); 10174 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 10175 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 10176 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 10177 list ~= "\n\twidth: " ~ toInternal!string(s.width); 10178 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 10179 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 10180 } 10181 10182 addInfo(s); 10183 10184 s = s.parent; 10185 while(s) { 10186 list ~= "\n"; 10187 addInfo(s); 10188 s = s.parent; 10189 } 10190 parentList.content = list; 10191 10192 clickX.label = toInternal!string(ev.clientX); 10193 clickY.label = toInternal!string(ev.clientY); 10194 }); 10195 } 10196 10197 EventListener[] parentListeners; 10198 10199 override void close() { 10200 assert(p !is null); 10201 foreach(p; parentListeners) 10202 p.disconnect(); 10203 parentListeners = null; 10204 p.devTools = null; 10205 p = null; 10206 super.close(); 10207 } 10208 10209 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10210 if(ev.key == Key.F12) { 10211 this.close(); 10212 if(p) 10213 p.devTools = null; 10214 } else { 10215 super.defaultEventHandler_keydown(ev); 10216 } 10217 } 10218 10219 void log(T...)(T t) { 10220 string str; 10221 import std.conv; 10222 foreach(i; t) 10223 str ~= to!string(i); 10224 str ~= "\n"; 10225 logWindow.addText(str); 10226 logWindow.scrollToBottom(); 10227 10228 //version(custom_widgets) 10229 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 10230 } 10231 } 10232 10233 /++ 10234 A dialog is a transient window that intends to get information from 10235 the user before being dismissed. 10236 +/ 10237 class Dialog : Window { 10238 /// 10239 this(Window parent, int width, int height, string title = null) { 10240 super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win); 10241 10242 // this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 10243 } 10244 10245 /// 10246 this(Window parent, string title, int width, int height) { 10247 this(parent, width, height, title); 10248 } 10249 10250 deprecated("Pass an explicit parent window, even if it is `null`") 10251 this(int width, int height, string title = null) { 10252 this(null, width, height, title); 10253 } 10254 10255 /// 10256 void OK() { 10257 10258 } 10259 10260 /// 10261 void Cancel() { 10262 this.close(); 10263 } 10264 } 10265 10266 /++ 10267 A custom widget similar to the HTML5 <details> tag. 10268 +/ 10269 version(none) 10270 class DetailsView : Widget { 10271 10272 } 10273 10274 // FIXME: maybe i should expose the other list views Windows offers too 10275 10276 /++ 10277 A TableView is a widget made to display a table of data strings. 10278 10279 10280 Future_Directions: 10281 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 10282 10283 I will add a selection changed event at some point, as well as item clicked events. 10284 History: 10285 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 10286 See_Also: 10287 [ListWidget] which displays a list of strings without additional columns. 10288 +/ 10289 class TableView : Widget { 10290 /++ 10291 10292 +/ 10293 this(Widget parent) { 10294 super(parent); 10295 10296 version(win32_widgets) { 10297 // LVS_EX_LABELTIP might be worth too 10298 // LVS_OWNERDRAWFIXED 10299 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//, LVS_EX_TRACKSELECT); // ex style for for LVN_HOTTRACK 10300 } else version(custom_widgets) { 10301 auto smw = new ScrollMessageWidget(this); 10302 smw.addDefaultKeyboardListeners(); 10303 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 10304 tvwi = new TableViewWidgetInner(this, smw); 10305 } 10306 } 10307 10308 // FIXME: auto-size columns on double click of header thing like in Windows 10309 // it need only make the currently displayed things fit well. 10310 10311 10312 private ColumnInfo[] columns; 10313 private int itemCount; 10314 10315 version(custom_widgets) private { 10316 TableViewWidgetInner tvwi; 10317 } 10318 10319 /// Passed to [setColumnInfo] 10320 static struct ColumnInfo { 10321 const(char)[] name; /// the name displayed in the header 10322 /++ 10323 The default width, in pixels. As a special case, you can set this to -1 10324 if you want the system to try to automatically size the width to fit visible 10325 content. If it can't, it will try to pick a sensible default size. 10326 10327 Any other negative value is not allowed and may lead to unpredictable results. 10328 10329 History: 10330 The -1 behavior was specified on December 3, 2021. It actually worked before 10331 anyway on Win32 but now it is a formal feature with partial Linux support. 10332 10333 Bugs: 10334 It doesn't actually attempt to calculate a best-fit width on Linux as of 10335 December 3, 2021. I do plan to fix this in the future, but Windows is the 10336 priority right now. At least it doesn't break things when you use it now. 10337 +/ 10338 int width; 10339 10340 /++ 10341 Alignment of the text in the cell. Applies to the header as well as all data in this 10342 column. 10343 10344 Bugs: 10345 On Windows, the first column ignores this member and is always left aligned. 10346 You can work around this by inserting a dummy first column with width = 0 10347 then putting your actual data in the second column, which does respect the 10348 alignment. 10349 10350 This is a quirk of the operating system's implementation going back a very 10351 long time and is unlikely to ever be fixed. 10352 +/ 10353 TextAlignment alignment; 10354 10355 /++ 10356 After all the pixel widths have been assigned, any left over 10357 space is divided up among all columns and distributed to according 10358 to the widthPercent field. 10359 10360 10361 For example, if you have two fields, both with width 50 and one with 10362 widthPercent of 25 and the other with widthPercent of 75, and the 10363 container is 200 pixels wide, first both get their width of 50. 10364 then the 100 remaining pixels are split up, so the one gets a total 10365 of 75 pixels and the other gets a total of 125. 10366 10367 This is automatically applied as the window is resized. 10368 10369 If there is not enough space - that is, when a horizontal scrollbar 10370 needs to appear - there are 0 pixels divided up, and thus everyone 10371 gets 0. This can cause a column to shrink out of proportion when 10372 passing the scroll threshold. 10373 10374 It is important to still set a fixed width (that is, to populate the 10375 `width` field) even if you use the percents because that will be the 10376 default minimum in the event of a scroll bar appearing. 10377 10378 The percents total in the column can never exceed 100 or be less than 0. 10379 Doing this will trigger an assert error. 10380 10381 Implementation note: 10382 10383 Please note that percentages are only recalculated 1) upon original 10384 construction and 2) upon resizing the control. If the user adjusts the 10385 width of a column, the percentage items will not be updated. 10386 10387 On the other hand, if the user adjusts the width of a percentage column 10388 then resizes the window, it is recalculated, meaning their hand adjustment 10389 is discarded. This specific behavior may change in the future as it is 10390 arguably a bug, but I'm not certain yet. 10391 10392 History: 10393 Added November 10, 2021 (dub v10.4) 10394 +/ 10395 int widthPercent; 10396 10397 10398 private int calculatedWidth; 10399 } 10400 /++ 10401 Sets the number of columns along with information about the headers. 10402 10403 Please note: on Windows, the first column ignores your alignment preference 10404 and is always left aligned. 10405 +/ 10406 void setColumnInfo(ColumnInfo[] columns...) { 10407 10408 foreach(ref c; columns) { 10409 c.name = c.name.idup; 10410 } 10411 this.columns = columns.dup; 10412 10413 updateCalculatedWidth(false); 10414 10415 version(custom_widgets) { 10416 tvwi.header.updateHeaders(); 10417 tvwi.updateScrolls(); 10418 } else version(win32_widgets) 10419 foreach(i, column; this.columns) { 10420 LVCOLUMN lvColumn; 10421 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 10422 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 10423 10424 auto bfr = WCharzBuffer(column.name); 10425 lvColumn.pszText = bfr.ptr; 10426 10427 if(column.alignment & TextAlignment.Center) 10428 lvColumn.fmt = LVCFMT_CENTER; 10429 else if(column.alignment & TextAlignment.Right) 10430 lvColumn.fmt = LVCFMT_RIGHT; 10431 else 10432 lvColumn.fmt = LVCFMT_LEFT; 10433 10434 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 10435 throw new WindowsApiException("Insert Column Fail", GetLastError()); 10436 } 10437 } 10438 10439 version(custom_widgets) 10440 private int getColumnSizeForContent(size_t columnIndex) { 10441 // FIXME: idk where the problem is but with a 2x scale the horizontal scroll is insuffiicent. i think the SMW is doing it wrong. 10442 // might also want a user-defined max size too 10443 int padding = scaleWithDpi(6); 10444 int m = this.defaultTextWidth(this.columns[columnIndex].name) + padding; 10445 10446 if(getData !is null) 10447 foreach(row; 0 .. itemCount) 10448 getData(row, cast(int) columnIndex, (txt) { 10449 m = mymax(m, this.defaultTextWidth(txt) + padding); 10450 }); 10451 10452 if(m < 32) 10453 m = 32; 10454 10455 return m; 10456 } 10457 10458 /++ 10459 History: 10460 Added February 26, 2025 10461 +/ 10462 void autoSizeColumnsToContent() { 10463 version(custom_widgets) { 10464 foreach(idx, ref c; columns) { 10465 c.width = getColumnSizeForContent(idx); 10466 } 10467 updateCalculatedWidth(false); 10468 tvwi.updateScrolls(); 10469 } else version(win32_widgets) { 10470 foreach(i, c; columns) 10471 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, LVSCW_AUTOSIZE); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 10472 } 10473 } 10474 10475 /++ 10476 History: 10477 Added March 1, 2025 10478 +/ 10479 bool supportsPerCellAlignment() { 10480 version(custom_widgets) 10481 return true; 10482 else version(win32_widgets) 10483 return false; 10484 return false; 10485 } 10486 10487 private int getActualSetSize(size_t i, bool askWindows) { 10488 version(win32_widgets) 10489 if(askWindows) 10490 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 10491 auto w = columns[i].width; 10492 if(w == -1) 10493 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 10494 return w; 10495 } 10496 10497 private void updateCalculatedWidth(bool informWindows) { 10498 int padding; 10499 version(win32_widgets) 10500 padding = 4; 10501 int remaining = this.width; 10502 foreach(i, column; columns) 10503 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 10504 remaining -= padding; 10505 if(remaining < 0) 10506 remaining = 0; 10507 10508 int percentTotal; 10509 foreach(i, ref column; columns) { 10510 percentTotal += column.widthPercent; 10511 10512 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 10513 10514 column.calculatedWidth = c; 10515 10516 version(win32_widgets) 10517 if(informWindows) 10518 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 10519 } 10520 10521 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 10522 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)."); 10523 10524 10525 } 10526 10527 override void registerMovement() { 10528 super.registerMovement(); 10529 10530 updateCalculatedWidth(true); 10531 } 10532 10533 /++ 10534 History: 10535 Added September 27, 2025 10536 +/ 10537 void scrollIntoView(int row, int column) { 10538 version(custom_widgets) { 10539 tvwi.smw.vsb.setPosition(row); 10540 int w; 10541 foreach(col; this.columns[0 .. column]) 10542 w += col.calculatedWidth; 10543 tvwi.smw.hsb.setPosition(w); 10544 tvwi.smw.notify(); 10545 10546 } else version(win32_widgets) { 10547 SendMessage(hwnd, LVM_ENSUREVISIBLE, row, true); 10548 } 10549 } 10550 10551 /++ 10552 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. 10553 +/ 10554 void setItemCount(int count) { 10555 this.itemCount = count; 10556 version(custom_widgets) { 10557 tvwi.updateScrolls(); 10558 redraw(); 10559 } else version(win32_widgets) { 10560 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 10561 } 10562 } 10563 10564 /++ 10565 Clears all items; 10566 +/ 10567 void clear() { 10568 this.itemCount = 0; 10569 this.columns = null; 10570 version(custom_widgets) { 10571 tvwi.header.updateHeaders(); 10572 tvwi.updateScrolls(); 10573 redraw(); 10574 } else version(win32_widgets) { 10575 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 10576 } 10577 } 10578 10579 /+ 10580 version(win32_widgets) 10581 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 10582 auto itemId = dis.itemID; 10583 auto hdc = dis.hDC; 10584 auto rect = dis.rcItem; 10585 switch(dis.itemAction) { 10586 case ODA_DRAWENTIRE: 10587 10588 // FIXME: do other items 10589 // FIXME: do the focus rectangle i guess 10590 // FIXME: alignment 10591 // FIXME: column width 10592 // FIXME: padding left 10593 // FIXME: check dpi scaling 10594 // FIXME: don't owner draw unless it is necessary. 10595 10596 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 10597 RECT itemRect; 10598 itemRect.top = 1; // subitem idx, 1-based 10599 itemRect.left = LVIR_BOUNDS; 10600 10601 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 10602 itemRect.left += padding; 10603 10604 getData(itemId, 0, (in char[] data) { 10605 auto wdata = WCharzBuffer(data); 10606 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 10607 10608 }); 10609 goto case; 10610 case ODA_FOCUS: 10611 if(dis.itemState & ODS_FOCUS) 10612 DrawFocusRect(hdc, &rect); 10613 break; 10614 case ODA_SELECT: 10615 // itemState & ODS_SELECTED 10616 break; 10617 default: 10618 } 10619 return 1; 10620 } 10621 +/ 10622 10623 version(win32_widgets) { 10624 CellStyle last; 10625 COLORREF defaultColor; 10626 COLORREF defaultBackground; 10627 } 10628 10629 version(win32_widgets) 10630 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 10631 switch(code) { 10632 case NM_CUSTOMDRAW: 10633 auto s = cast(NMLVCUSTOMDRAW*) hdr; 10634 switch(s.nmcd.dwDrawStage) { 10635 case CDDS_PREPAINT: 10636 if(getCellStyle is null) 10637 return 0; 10638 10639 mustReturn = true; 10640 return CDRF_NOTIFYITEMDRAW; 10641 case CDDS_ITEMPREPAINT: 10642 mustReturn = true; 10643 return CDRF_NOTIFYSUBITEMDRAW; 10644 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 10645 mustReturn = true; 10646 10647 if(getCellStyle is null) // this SHOULD never happen... 10648 return 0; 10649 10650 if(s.iSubItem == 0) { 10651 // Windows resets it per row so we'll use item 0 as a chance 10652 // to capture these for later 10653 defaultColor = s.clrText; 10654 defaultBackground = s.clrTextBk; 10655 } 10656 10657 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 10658 // if no special style and no reset needed... 10659 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 10660 return 0; // allow default processing to continue 10661 10662 last = style; 10663 10664 // might still need to reset or use the preference. 10665 10666 if(style.flags & CellStyle.Flags.textColorSet) 10667 s.clrText = style.textColor.asWindowsColorRef; 10668 else 10669 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 10670 if(style.flags & CellStyle.Flags.backgroundColorSet) 10671 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 10672 else 10673 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 10674 10675 return CDRF_NEWFONT; 10676 default: 10677 return 0; 10678 10679 } 10680 case NM_RETURN: // no need since i subclass keydown 10681 break; 10682 case LVN_COLUMNCLICK: 10683 auto info = cast(LPNMLISTVIEW) hdr; 10684 // FIXME can i get the button? 10685 this.emit!HeaderClickedEvent(info.iSubItem, MouseButton.left); 10686 break; 10687 case (LVN_FIRST-21) /* LVN_HOTTRACK */: 10688 // requires LVS_EX_TRACKSELECT 10689 // sdpyPrintDebugString("here"); 10690 mustReturn = 1; // override Windows' auto selection 10691 break; 10692 case NM_CLICK: 10693 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10694 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); 10695 break; 10696 case NM_DBLCLK: 10697 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10698 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); 10699 break; 10700 case NM_RCLICK: 10701 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10702 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); 10703 break; 10704 case NM_RDBLCLK: 10705 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10706 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); 10707 break; 10708 case LVN_GETDISPINFO: 10709 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 10710 if(info.item.mask & LVIF_TEXT) { 10711 if(getData) { 10712 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 10713 auto bfr = WCharzBuffer(dataReceived); 10714 auto len = info.item.cchTextMax; 10715 if(bfr.length < len) 10716 len = cast(typeof(len)) bfr.length; 10717 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 10718 info.item.pszText[len] = 0; 10719 }); 10720 } else { 10721 info.item.pszText[0] = 0; 10722 } 10723 //info.item.iItem 10724 //if(info.item.iSubItem) 10725 } 10726 break; 10727 default: 10728 } 10729 return 0; 10730 } 10731 10732 // FIXME: this throws off mouse calculations, it should only happen when we're at the top level or something idk 10733 override bool encapsulatedChildren() { 10734 return true; 10735 } 10736 10737 /++ 10738 Informs the control that content has changed. 10739 10740 History: 10741 Added November 10, 2021 (dub v10.4) 10742 +/ 10743 void update() { 10744 version(custom_widgets) 10745 redraw(); 10746 else { 10747 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 10748 UpdateWindow(hwnd); 10749 } 10750 10751 10752 } 10753 10754 /++ 10755 Called by the system to request the text content of an individual cell. You 10756 should pass the text into the provided `sink` delegate. This function will be 10757 called for each visible cell as-needed when drawing. 10758 +/ 10759 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 10760 10761 /++ 10762 Available per-cell style customization options. Use one of the constructors 10763 provided to set the values conveniently, or default construct it and set individual 10764 values yourself. Just remember to set the `flags` so your values are actually used. 10765 If the flag isn't set, the field is ignored and the system default is used instead. 10766 10767 This is returned by the [getCellStyle] delegate. 10768 10769 Examples: 10770 --- 10771 // assumes you have a variables called `my_data` which is an array of arrays of numbers 10772 auto table = new TableView(window); 10773 // snip: you would set up columns here 10774 10775 // this is how you provide data to the table view class 10776 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 10777 import std.conv; 10778 sink(to!string(my_data[row][column])); 10779 }; 10780 10781 // and this is how you customize the colors 10782 table.getCellStyle = delegate(int row, int column) { 10783 return (my_data[row][column] < 0) ? 10784 TableView.CellStyle(Color.red); // make negative numbers red 10785 : TableView.CellStyle.init; // leave the rest alone 10786 }; 10787 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 10788 --- 10789 10790 History: 10791 Added November 27, 2021 (dub v10.4) 10792 +/ 10793 struct CellStyle { 10794 /// 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. 10795 this(Color textColor) { 10796 this.textColor = textColor; 10797 this.flags |= Flags.textColorSet; 10798 } 10799 /// Sets a custom text and background color. 10800 this(Color textColor, Color backgroundColor) { 10801 this.textColor = textColor; 10802 this.backgroundColor = backgroundColor; 10803 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 10804 } 10805 /++ 10806 Alignment is only supported on some platforms. 10807 +/ 10808 this(TextAlignment alignment) { 10809 this.alignment = alignment; 10810 this.flags |= Flags.alignmentSet; 10811 } 10812 /// ditto 10813 this(TextAlignment alignment, Color textColor) { 10814 this.alignment = alignment; 10815 this.textColor = textColor; 10816 this.flags |= Flags.alignmentSet | Flags.textColorSet; 10817 } 10818 /// ditto 10819 this(TextAlignment alignment, Color textColor, Color backgroundColor) { 10820 this.alignment = alignment; 10821 this.textColor = textColor; 10822 this.backgroundColor = backgroundColor; 10823 this.flags |= Flags.alignmentSet | Flags.textColorSet | Flags.backgroundColorSet; 10824 } 10825 10826 TextAlignment alignment; 10827 Color textColor; 10828 Color backgroundColor; 10829 int flags; /// bitmask of [Flags] 10830 /// available options to combine into [flags] 10831 enum Flags { 10832 textColorSet = 1 << 0, 10833 backgroundColorSet = 1 << 1, 10834 alignmentSet = 1 << 2, 10835 } 10836 } 10837 /++ 10838 Companion delegate to [getData] that allows you to custom style each 10839 cell of the table. 10840 10841 Returns: 10842 A [CellStyle] structure that describes the desired style for the 10843 given cell. `return CellStyle.init` if you want the default style. 10844 10845 History: 10846 Added November 27, 2021 (dub v10.4) 10847 +/ 10848 CellStyle delegate(int row, int column) getCellStyle; 10849 10850 // i want to be able to do things like draw little colored things to show red for negative numbers 10851 // or background color indicators or even in-cell charts 10852 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 10853 10854 /++ 10855 When the user clicks on a header, this event is emitted. It has a member to identify which header (by index) was clicked. 10856 +/ 10857 mixin Emits!HeaderClickedEvent; 10858 10859 /++ 10860 History: 10861 Added March 2, 2025 10862 +/ 10863 mixin Emits!CellClickedEvent; 10864 } 10865 10866 /++ 10867 This is emitted by the [TableView] when a user clicks on a column header. 10868 10869 Its member `columnIndex` has the zero-based index of the column that was clicked. 10870 10871 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 10872 10873 History: 10874 Added November 27, 2021 (dub v10.4) 10875 10876 Made `final` on January 3, 2025 10877 +/ 10878 final class HeaderClickedEvent : Event { 10879 enum EventString = "HeaderClicked"; 10880 this(Widget target, int columnIndex, MouseButton button) { 10881 this.columnIndex = columnIndex; 10882 this.button = button; 10883 super(EventString, target); 10884 } 10885 10886 /// The index of the column 10887 int columnIndex; 10888 10889 /++ 10890 History: 10891 Added September 27, 2025 10892 Bugs: 10893 Not implemented on Windows, always sets mouse button left. 10894 +/ 10895 MouseButton button; 10896 10897 /// 10898 override @property int intValue() { 10899 return columnIndex; 10900 } 10901 } 10902 10903 /++ 10904 History: 10905 Added March 2, 2025 10906 +/ 10907 final class CellClickedEvent : MouseEventBase { 10908 enum EventString = "CellClicked"; 10909 this(Widget target, int rowIndex, int columnIndex, MouseButton button, MouseButtonLinear mouseButtonLinear, int x, int y, bool altKey, bool ctrlKey, bool shiftKey, bool isDoubleClick) { 10910 this.rowIndex = rowIndex; 10911 this.columnIndex = columnIndex; 10912 this.button = button; 10913 this.buttonLinear = mouseButtonLinear; 10914 this.isDoubleClick = isDoubleClick; 10915 this.clientX = x; 10916 this.clientY = y; 10917 10918 this.altKey = altKey; 10919 this.ctrlKey = ctrlKey; 10920 this.shiftKey = shiftKey; 10921 10922 // import std.stdio; std.stdio.writeln(rowIndex, "x", columnIndex, " @ ", x, ",", y, " ", button, " ", isDoubleClick, " ", altKey, " ", ctrlKey, " ", shiftKey); 10923 10924 // FIXME: x, y, state, altButton etc? 10925 super(EventString, target); 10926 } 10927 10928 /++ 10929 See also: [button] inherited from the base class. 10930 10931 clientX and clientY are irrespective of scrolling - FIXME is that sane? 10932 +/ 10933 int columnIndex; 10934 10935 /// ditto 10936 int rowIndex; 10937 10938 /// ditto 10939 bool isDoubleClick; 10940 10941 /+ 10942 // i could do intValue as a linear index if we know the width 10943 // and a stringValue with the string in the cell. but idk if worth. 10944 override @property int intValue() { 10945 return columnIndex; 10946 } 10947 +/ 10948 10949 } 10950 10951 version(custom_widgets) 10952 private class TableViewWidgetInner : Widget { 10953 10954 // wrap this thing in a ScrollMessageWidget 10955 10956 TableView tvw; 10957 ScrollMessageWidget smw; 10958 HeaderWidget header; 10959 10960 this(TableView tvw, ScrollMessageWidget smw) { 10961 this.tvw = tvw; 10962 this.smw = smw; 10963 super(smw); 10964 10965 this.tabStop = true; 10966 10967 header = new HeaderWidget(this, smw.getHeader()); 10968 10969 smw.addEventListener("scroll", () { 10970 this.redraw(); 10971 header.redraw(); 10972 }); 10973 10974 10975 // I need headers outside the scroll area but rendered on the same line as the up arrow 10976 // FIXME: add a fixed header to the SMW 10977 } 10978 10979 enum padding = 3; 10980 10981 void updateScrolls() { 10982 int w; 10983 foreach(idx, column; tvw.columns) { 10984 w += column.calculatedWidth; 10985 } 10986 smw.setTotalArea(w, tvw.itemCount); 10987 columnsWidth = w; 10988 } 10989 10990 private int columnsWidth; 10991 10992 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 10993 10994 override void registerMovement() { 10995 super.registerMovement(); 10996 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 10997 smw.setViewableArea(this.width, this.height / lh); 10998 } 10999 11000 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11001 int x; 11002 int y; 11003 11004 int row = smw.position.y; 11005 11006 foreach(lol; 0 .. this.height / lh) { 11007 if(row >= tvw.itemCount) 11008 break; 11009 x = 0; 11010 foreach(columnNumber, column; tvw.columns) { 11011 auto x2 = x + column.calculatedWidth; 11012 auto smwx = smw.position.x; 11013 11014 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 11015 auto startX = x; 11016 auto endX = x + column.calculatedWidth; 11017 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 11018 case TextAlignment.Left: startX += padding; break; 11019 case TextAlignment.Center: startX += padding; endX -= padding; break; 11020 case TextAlignment.Right: endX -= padding; break; 11021 default: /* broken */ break; 11022 } 11023 if(column.width != 0) // no point drawing an invisible column 11024 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 11025 auto endClip = endX - smw.position.x; 11026 if(endClip > this.width - padding) 11027 endClip = this.width - padding; 11028 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endClip, y + lh))); 11029 11030 void dotext(WidgetPainter painter, TextAlignment alignment) { 11031 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x - padding, y + lh), alignment); 11032 } 11033 11034 if(tvw.getCellStyle !is null) { 11035 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 11036 11037 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 11038 auto tempPainter = painter; 11039 tempPainter.fillColor = style.backgroundColor; 11040 tempPainter.outlineColor = style.backgroundColor; 11041 11042 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 11043 Point(endX - smw.position.x, y + lh)); 11044 } 11045 auto tempPainter = painter; 11046 if(style.flags & TableView.CellStyle.Flags.textColorSet) 11047 tempPainter.outlineColor = style.textColor; 11048 11049 auto alignment = column.alignment; 11050 if(style.flags & TableView.CellStyle.Flags.alignmentSet) 11051 alignment = style.alignment; 11052 dotext(tempPainter, alignment); 11053 } else { 11054 dotext(painter, column.alignment); 11055 } 11056 }); 11057 } 11058 11059 x += column.calculatedWidth; 11060 } 11061 row++; 11062 y += lh; 11063 } 11064 return bounds; 11065 } 11066 11067 static class Style : Widget.Style { 11068 override WidgetBackground background() { 11069 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 11070 } 11071 } 11072 mixin OverrideStyle!Style; 11073 11074 private static class HeaderWidget : Widget { 11075 /+ 11076 maybe i should do a splitter thing on top of the other widgets 11077 so the splitter itself isn't really drawn but still replies to mouse events? 11078 +/ 11079 this(TableViewWidgetInner tvw, Widget parent) { 11080 super(parent); 11081 this.tvw = tvw; 11082 11083 this.remainder = new Button("", this); 11084 11085 this.addEventListener((scope ClickEvent ev) { 11086 int header = -1; 11087 foreach(idx, child; this.children[1 .. $]) { 11088 if(child is ev.target) { 11089 header = cast(int) idx; 11090 break; 11091 } 11092 } 11093 11094 if(header != -1) { 11095 auto hce = new HeaderClickedEvent(tvw.tvw, header, cast(MouseButton) ev.button); 11096 hce.dispatch(); 11097 } 11098 11099 }); 11100 } 11101 11102 override int minHeight() { 11103 return defaultLineHeight + 4; // same as Button 11104 } 11105 11106 void updateHeaders() { 11107 foreach(child; children[1 .. $]) 11108 child.removeWidget(); 11109 11110 foreach(column; tvw.tvw.columns) { 11111 // the cast is ok because I dup it above, just the type is never changed. 11112 // all this is private so it should never get messed up. 11113 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 11114 } 11115 } 11116 11117 Button remainder; 11118 TableViewWidgetInner tvw; 11119 11120 override void recomputeChildLayout() { 11121 registerMovement(); 11122 int pos; 11123 foreach(idx, child; children[1 .. $]) { 11124 if(idx >= tvw.tvw.columns.length) 11125 continue; 11126 child.x = pos; 11127 child.y = 0; 11128 child.width = tvw.tvw.columns[idx].calculatedWidth; 11129 child.height = scaleWithDpi(16);// this.height; 11130 pos += child.width; 11131 11132 child.recomputeChildLayout(); 11133 } 11134 11135 if(remainder is null) 11136 return; 11137 11138 remainder.x = pos; 11139 remainder.y = 0; 11140 if(pos < this.width) 11141 remainder.width = this.width - pos;// + 4; 11142 else 11143 remainder.width = 0; 11144 remainder.height = scaleWithDpi(16); 11145 11146 remainder.recomputeChildLayout(); 11147 } 11148 11149 // for the scrollable children mixin 11150 Point scrollOrigin() { 11151 return Point(tvw.smw.position.x, 0); 11152 } 11153 void paintFrameAndBackground(WidgetPainter painter) { } 11154 11155 // for mouse event dispatching 11156 override protected void addScrollPosition(ref int x, ref int y) { 11157 x += scrollOrigin.x; 11158 y += scrollOrigin.y; 11159 } 11160 11161 mixin ScrollableChildren; 11162 } 11163 11164 private void emitCellClickedEvent(scope MouseEventBase event, bool isDoubleClick) { 11165 int mx = event.clientX + smw.position.x; 11166 int my = event.clientY; 11167 11168 Widget par = this; 11169 while(par && !par.encapsulatedChildren) { 11170 my -= par.y; // to undo the encapsulatedChildren adjustClientCoordinates effect 11171 par = par.parent; 11172 } 11173 if(par is null) 11174 my = event.clientY; // encapsulatedChildren not present? 11175 11176 int row = my / lh + smw.position.y; // scrolling here is done per-item, not per pixel 11177 if(row > tvw.itemCount) 11178 row = -1; 11179 11180 int column = -1; 11181 if(row != -1) { 11182 int pos; 11183 foreach(idx, col; tvw.columns) { 11184 pos += col.calculatedWidth; 11185 if(mx < pos) { 11186 column = cast(int) idx; 11187 break; 11188 } 11189 } 11190 } 11191 11192 // wtf are these casts about? 11193 tvw.emit!CellClickedEvent(row, column, cast(MouseButton) event.button, cast(MouseButtonLinear) event.buttonLinear, event.clientX, event.clientY, event.altKey, event.ctrlKey, event.shiftKey, isDoubleClick); 11194 } 11195 11196 override void defaultEventHandler_click(scope ClickEvent ce) { 11197 // FIXME: should i filter mouse wheel events? Windows doesn't send them but i can. 11198 emitCellClickedEvent(ce, false); 11199 } 11200 11201 override void defaultEventHandler_dblclick(scope DoubleClickEvent ce) { 11202 emitCellClickedEvent(ce, true); 11203 } 11204 } 11205 11206 /+ 11207 11208 // given struct / array / number / string / etc, make it viewable and editable 11209 class DataViewerWidget : Widget { 11210 11211 } 11212 +/ 11213 11214 /++ 11215 A line edit box with an associated label. 11216 11217 History: 11218 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 11219 11220 ``` 11221 Old: ________ 11222 11223 New: 11224 ____________ 11225 ``` 11226 11227 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 11228 11229 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 11230 horizontal label but left aligned. You may also consider a [GridLayout]. 11231 +/ 11232 alias LabeledLineEdit = Labeled!LineEdit; 11233 11234 private int widthThatWouldFitChildLabels(Widget w) { 11235 if(w is null) 11236 return 0; 11237 11238 int max; 11239 11240 if(auto label = cast(TextLabel) w) { 11241 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 11242 } else { 11243 foreach(child; w.children) { 11244 max = mymax(max, widthThatWouldFitChildLabels(child)); 11245 } 11246 } 11247 11248 return max; 11249 } 11250 11251 /++ 11252 History: 11253 Added May 19, 2021 11254 +/ 11255 class Labeled(T) : Widget { 11256 /// 11257 this(string label, Widget parent) { 11258 super(parent); 11259 initialize!VerticalLayout(label, TextAlignment.Left, parent); 11260 } 11261 11262 /++ 11263 History: 11264 The alignment parameter was added May 17, 2021 11265 +/ 11266 this(string label, TextAlignment alignment, Widget parent) { 11267 super(parent); 11268 initialize!HorizontalLayout(label, alignment, parent); 11269 } 11270 11271 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 11272 tabStop = false; 11273 horizontal = is(L == HorizontalLayout); 11274 auto hl = new L(this); 11275 if(horizontal) { 11276 static class SpecialTextLabel : TextLabel { 11277 Widget outerParent; 11278 11279 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 11280 this.outerParent = outerParent; 11281 super(label, alignment, parent); 11282 } 11283 11284 override int flexBasisWidth() { 11285 return widthThatWouldFitChildLabels(outerParent); 11286 } 11287 /+ 11288 override int widthShrinkiness() { return 0; } 11289 override int widthStretchiness() { return 1; } 11290 +/ 11291 11292 override int paddingRight() { return 6; } 11293 override int paddingLeft() { return 9; } 11294 11295 override int paddingTop() { return 3; } 11296 } 11297 this.label = new SpecialTextLabel(label, alignment, parent, hl); 11298 } else 11299 this.label = new TextLabel(label, alignment, hl); 11300 this.lineEdit = new T(hl); 11301 11302 this.label.labelFor = this.lineEdit; 11303 } 11304 11305 private bool horizontal; 11306 11307 TextLabel label; /// 11308 T lineEdit; /// 11309 11310 override int flexBasisWidth() { return 250; } 11311 override int widthShrinkiness() { return 1; } 11312 11313 override int minHeight() { 11314 return this.children[0].minHeight; 11315 } 11316 override int maxHeight() { return minHeight(); } 11317 override int marginTop() { return 4; } 11318 override int marginBottom() { return 4; } 11319 11320 // FIXME: i should prolly call it value as well as content tbh 11321 11322 /// 11323 @property string content() { 11324 return lineEdit.content; 11325 } 11326 /// 11327 @property void content(string c) { 11328 return lineEdit.content(c); 11329 } 11330 11331 /// 11332 void selectAll() { 11333 lineEdit.selectAll(); 11334 } 11335 11336 override void focus() { 11337 lineEdit.focus(); 11338 } 11339 } 11340 11341 /++ 11342 A labeled password edit. 11343 11344 History: 11345 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 11346 11347 The default parameters for the constructors were also removed on May 19, 2021 11348 +/ 11349 alias LabeledPasswordEdit = Labeled!PasswordEdit; 11350 11351 private string toMenuLabel(string s) { 11352 string n; 11353 n.reserve(s.length); 11354 foreach(c; s) 11355 if(c == '_') 11356 n ~= ' '; 11357 else 11358 n ~= c; 11359 return n; 11360 } 11361 11362 private void autoExceptionHandler(Exception e) { 11363 messageBox(e.msg); 11364 } 11365 11366 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) { 11367 makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))(); 11368 } 11369 11370 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) { 11371 static if(is(T : void delegate())) { 11372 return () { 11373 try 11374 t(); 11375 catch(Exception e) 11376 autoExceptionHandler(e); 11377 }; 11378 } else static if(is(typeof(fn) Params == __parameters)) { 11379 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 11380 return () { 11381 void onOK(string s) { 11382 member = s; 11383 try 11384 t(Params[0](s)); 11385 catch(Exception e) 11386 autoExceptionHandler(e); 11387 } 11388 11389 if( 11390 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 11391 || type == FileDialogType.Save) 11392 { 11393 getSaveFileName(window, &onOK, member, filters, null); 11394 } else 11395 getOpenFileName(window, &onOK, member, filters, null); 11396 }; 11397 } else { 11398 struct S { 11399 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 11400 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 11401 } else mixin(q{ 11402 static foreach(idx, ignore; Params) { 11403 mixin("@(__traits(getAttributes, Params[idx .. idx + 1])) Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 11404 } 11405 }); 11406 } 11407 return () { 11408 dialog(window, (S s) { 11409 try { 11410 static if(is(typeof(t) Ret == return)) { 11411 static if(is(Ret == void)) { 11412 t(s.tupleof); 11413 } else { 11414 auto ret = t(s.tupleof); 11415 import std.conv; 11416 messageBox(to!string(ret), "Returned Value"); 11417 } 11418 } 11419 } catch(Exception e) 11420 autoExceptionHandler(e); 11421 }, null, __traits(identifier, fn)); 11422 }; 11423 } 11424 } else static assert(0, fn.stringof ~ " isn't a function but in a menu block"); 11425 } 11426 11427 private template hasAnyRelevantAnnotations(a...) { 11428 bool helper() { 11429 bool any; 11430 foreach(attr; a) { 11431 static if(is(typeof(attr) == .menu)) 11432 any = true; 11433 else static if(is(typeof(attr) == .toolbar)) 11434 any = true; 11435 else static if(is(attr == .separator)) 11436 any = true; 11437 else static if(is(typeof(attr) == .accelerator)) 11438 any = true; 11439 else static if(is(typeof(attr) == .hotkey)) 11440 any = true; 11441 else static if(is(typeof(attr) == .icon)) 11442 any = true; 11443 else static if(is(typeof(attr) == .label)) 11444 any = true; 11445 else static if(is(typeof(attr) == .tip)) 11446 any = true; 11447 } 11448 return any; 11449 } 11450 11451 enum bool hasAnyRelevantAnnotations = helper(); 11452 } 11453 11454 /++ 11455 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. 11456 +/ 11457 class MainWindow : Window { 11458 /// 11459 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 11460 super(initialWidth, initialHeight, title); 11461 11462 _clientArea = new ClientAreaWidget(); 11463 _clientArea.x = 0; 11464 _clientArea.y = 0; 11465 _clientArea.width = this.width; 11466 _clientArea.height = this.height; 11467 _clientArea.tabStop = false; 11468 11469 super.addChild(_clientArea); 11470 11471 statusBar = new StatusBar(this); 11472 } 11473 11474 /++ 11475 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). 11476 11477 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')`. 11478 11479 You can also use `@separator` to put a separating line in the menu before the function. 11480 11481 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. 11482 11483 Let's look at a complete example: 11484 11485 --- 11486 import arsd.minigui; 11487 11488 void main() { 11489 auto window = new MainWindow(); 11490 11491 // we can add widgets before or after setting the menu, either way is fine. 11492 // i'll do it before here so the local variables are available to the commands. 11493 11494 auto textEdit = new TextEdit(window); 11495 11496 // Remember, in D, you can define structs inside of functions 11497 // and those structs can access the function's local variables. 11498 // 11499 // Of course, you might also want to do this separately, and if you 11500 // do, make sure you keep a reference to the window as a struct data 11501 // member so you can refer to it in cases like this Exit function. 11502 struct Commands { 11503 // the & in the string indicates that the next letter is the hotkey 11504 // to access it from the keyboard (so here, alt+f will open the 11505 // file menu) 11506 @menu("&File") { 11507 @accelerator("Ctrl+N") 11508 @hotkey('n') 11509 @icon(GenericIcons.New) // add an icon to the action 11510 @toolbar("File") // adds it to a toolbar. 11511 // The toolbar name is never visible to the user, but is used to group icons. 11512 void New() { 11513 previousFileReferenced = null; 11514 textEdit.content = ""; 11515 } 11516 11517 @icon(GenericIcons.Open) 11518 @toolbar("File") 11519 @hotkey('s') 11520 @accelerator("Ctrl+O") 11521 void Open(FileName!() filename) { 11522 import std.file; 11523 textEdit.content = std.file.readText(filename); 11524 } 11525 11526 @icon(GenericIcons.Save) 11527 @toolbar("File") 11528 @accelerator("Ctrl+S") 11529 @hotkey('s') 11530 void Save() { 11531 // these are still functions, so of course you can 11532 // still call them yourself too 11533 Save_As(previousFileReferenced); 11534 } 11535 11536 // underscores translate to spaces in the visible name 11537 @hotkey('a') 11538 void Save_As(FileName!() filename) { 11539 import std.file; 11540 std.file.write(previousFileReferenced, textEdit.content); 11541 } 11542 11543 // you can put the annotations before or after the function name+args and it works the same way 11544 @separator 11545 void Exit() @accelerator("Alt+F4") @hotkey('x') { 11546 window.close(); 11547 } 11548 } 11549 11550 @menu("&Edit") { 11551 // not putting accelerators here because the text edit widget 11552 // does it locally, so no need to duplicate it globally. 11553 11554 @icon(GenericIcons.Undo) 11555 void Undo() @toolbar("Undo") { 11556 textEdit.undo(); 11557 } 11558 11559 @separator 11560 11561 @icon(GenericIcons.Cut) 11562 void Cut() @toolbar("Edit") { 11563 textEdit.cut(); 11564 } 11565 @icon(GenericIcons.Copy) 11566 void Copy() @toolbar("Edit") { 11567 textEdit.copy(); 11568 } 11569 @icon(GenericIcons.Paste) 11570 void Paste() @toolbar("Edit") { 11571 textEdit.paste(); 11572 } 11573 11574 @separator 11575 void Select_All() { 11576 textEdit.selectAll(); 11577 } 11578 } 11579 11580 @menu("Help") { 11581 void About() @accelerator("F1") { 11582 window.messageBox("A minigui sample program."); 11583 } 11584 11585 // @label changes the name in the menu from what is in the code 11586 @label("In Menu Name") 11587 void otherNameInCode() {} 11588 } 11589 } 11590 11591 // declare the object that holds the commands, and set 11592 // and members you want from it 11593 Commands commands; 11594 11595 // and now tell minigui to do its magic and create the ui for it! 11596 window.setMenuAndToolbarFromAnnotatedCode(commands); 11597 11598 // then, loop the window normally; 11599 window.loop(); 11600 11601 // important to note that the `commands` variable must live through the window's whole life cycle, 11602 // or you can have crashes. If you declare the variable and loop in different functions, make sure 11603 // you do `new Commands` so the garbage collector can take over management of it for you. 11604 } 11605 --- 11606 11607 Note that you can call this function multiple times and it will add the items in order to the given items. 11608 11609 +/ 11610 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 11611 setMenuAndToolbarFromAnnotatedCode_internal(t); 11612 } 11613 /// ditto 11614 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 11615 setMenuAndToolbarFromAnnotatedCode_internal(t); 11616 } 11617 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 11618 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 11619 Menu[string] mcs; 11620 11621 alias ToolbarSection = ToolBar.ToolbarSection; 11622 ToolbarSection[] toolbarSections; 11623 11624 foreach(menu; menuBar.subMenus) { 11625 mcs[menu.label] = menu; 11626 } 11627 11628 foreach(memberName; __traits(derivedMembers, T)) { 11629 static if(memberName != "this") 11630 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 11631 .menu menu; 11632 .toolbar toolbar; 11633 bool separator; 11634 .accelerator accelerator; 11635 .hotkey hotkey; 11636 .icon icon; 11637 string label; 11638 string tip; 11639 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 11640 static if(is(typeof(attr) == .menu)) 11641 menu = attr; 11642 else static if(is(typeof(attr) == .toolbar)) 11643 toolbar = attr; 11644 else static if(is(attr == .separator)) 11645 separator = true; 11646 else static if(is(typeof(attr) == .accelerator)) 11647 accelerator = attr; 11648 else static if(is(typeof(attr) == .hotkey)) 11649 hotkey = attr; 11650 else static if(is(typeof(attr) == .icon)) 11651 icon = attr; 11652 else static if(is(typeof(attr) == .label)) 11653 label = attr.label; 11654 else static if(is(typeof(attr) == .tip)) 11655 tip = attr.tip; 11656 } 11657 11658 if(menu !is .menu.init || toolbar !is .toolbar.init) { 11659 ushort correctIcon = icon.id; // FIXME 11660 if(label.length == 0) 11661 label = memberName.toMenuLabel; 11662 11663 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName)); 11664 11665 auto action = new Action(label, correctIcon, handler); 11666 11667 if(accelerator.keyString.length) { 11668 auto ke = KeyEvent.parse(accelerator.keyString); 11669 action.accelerator = ke; 11670 accelerators[ke.toStr] = handler; 11671 } 11672 11673 if(toolbar !is .toolbar.init) { 11674 bool found; 11675 foreach(ref section; toolbarSections) 11676 if(section.name == toolbar.groupName) { 11677 section.actions ~= action; 11678 found = true; 11679 break; 11680 } 11681 if(!found) { 11682 toolbarSections ~= ToolbarSection(toolbar.groupName, [action]); 11683 } 11684 } 11685 if(menu !is .menu.init) { 11686 Menu mc; 11687 if(menu.name in mcs) { 11688 mc = mcs[menu.name]; 11689 } else { 11690 mc = new Menu(menu.name, this); 11691 menuBar.addItem(mc); 11692 mcs[menu.name] = mc; 11693 } 11694 11695 if(separator) 11696 mc.addSeparator(); 11697 auto mi = mc.addItem(new MenuItem(action)); 11698 11699 if(hotkey !is .hotkey.init) 11700 mi.hotkey = hotkey.ch; 11701 } 11702 } 11703 } 11704 } 11705 11706 this.menuBar = menuBar; 11707 11708 if(toolbarSections.length) { 11709 auto tb = new ToolBar(toolbarSections, this); 11710 } 11711 } 11712 11713 void delegate()[string] accelerators; 11714 11715 override void defaultEventHandler_keydown(KeyDownEvent event) { 11716 auto str = event.originalKeyEvent.toStr; 11717 if(auto acl = str in accelerators) 11718 (*acl)(); 11719 11720 // Windows this this automatically so only on custom need we implement it 11721 version(custom_widgets) { 11722 if(event.altKey && this.menuBar) { 11723 foreach(item; this.menuBar.items) { 11724 if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) { 11725 // FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code 11726 item.dynamicState = DynamicState.hover | DynamicState.depressed; 11727 item.redraw(); 11728 auto e = new MouseDownEvent(item); 11729 e.dispatch(); 11730 break; 11731 } 11732 } 11733 } 11734 11735 if(event.key == Key.Menu) { 11736 showContextMenu(-1, -1); 11737 } 11738 } 11739 11740 super.defaultEventHandler_keydown(event); 11741 } 11742 11743 override void defaultEventHandler_mouseover(MouseOverEvent event) { 11744 super.defaultEventHandler_mouseover(event); 11745 if(this.statusBar !is null && event.target.statusTip.length) 11746 this.statusBar.parts[0].content = event.target.statusTip; 11747 else if(this.statusBar !is null && this.statusTip.length) 11748 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 11749 } 11750 11751 override void addChild(Widget c, int position = int.max) { 11752 if(auto tb = cast(ToolBar) c) 11753 version(win32_widgets) 11754 super.addChild(c, 0); 11755 else version(custom_widgets) 11756 super.addChild(c, menuBar ? 1 : 0); 11757 else static assert(0); 11758 else 11759 clientArea.addChild(c, position); 11760 } 11761 11762 ToolBar _toolBar; 11763 /// 11764 ToolBar toolBar() { return _toolBar; } 11765 /// 11766 ToolBar toolBar(ToolBar t) { 11767 _toolBar = t; 11768 foreach(child; this.children) 11769 if(child is t) 11770 return t; 11771 version(win32_widgets) 11772 super.addChild(t, 0); 11773 else version(custom_widgets) 11774 super.addChild(t, menuBar ? 1 : 0); 11775 else static assert(0); 11776 return t; 11777 } 11778 11779 MenuBar _menu; 11780 /// 11781 MenuBar menuBar() { return _menu; } 11782 /// 11783 MenuBar menuBar(MenuBar m) { 11784 if(m is _menu) { 11785 version(custom_widgets) 11786 queueRecomputeChildLayout(); 11787 return m; 11788 } 11789 11790 if(_menu !is null) { 11791 // make sure it is sanely removed 11792 // FIXME 11793 } 11794 11795 _menu = m; 11796 11797 version(win32_widgets) { 11798 SetMenu(parentWindow.win.impl.hwnd, m.handle); 11799 } else version(custom_widgets) { 11800 super.addChild(m, 0); 11801 11802 // clientArea.y = menu.height; 11803 // clientArea.height = this.height - menu.height; 11804 11805 queueRecomputeChildLayout(); 11806 } else static assert(false); 11807 11808 return _menu; 11809 } 11810 private Widget _clientArea; 11811 /// 11812 @property Widget clientArea() { return _clientArea; } 11813 protected @property void clientArea(Widget wid) { 11814 _clientArea = wid; 11815 } 11816 11817 private StatusBar _statusBar; 11818 /++ 11819 Returns the window's [StatusBar]. Be warned it may be `null`. 11820 +/ 11821 @property StatusBar statusBar() { return _statusBar; } 11822 /// ditto 11823 @property void statusBar(StatusBar bar) { 11824 if(_statusBar !is null) 11825 _statusBar.removeWidget(); 11826 _statusBar = bar; 11827 if(bar !is null) 11828 super.addChild(_statusBar); 11829 } 11830 } 11831 11832 /+ 11833 This is really an implementation detail of [MainWindow] 11834 +/ 11835 private class ClientAreaWidget : Widget { 11836 this() { 11837 this.tabStop = false; 11838 super(null); 11839 //sa = new ScrollableWidget(this); 11840 } 11841 /* 11842 ScrollableWidget sa; 11843 override void addChild(Widget w, int position) { 11844 if(sa is null) 11845 super.addChild(w, position); 11846 else { 11847 sa.addChild(w, position); 11848 sa.setContentSize(this.minWidth + 1, this.minHeight); 11849 writeln(sa.contentWidth, "x", sa.contentHeight); 11850 } 11851 } 11852 */ 11853 } 11854 11855 /** 11856 Toolbars are lists of buttons (typically icons) that appear under the menu. 11857 Each button ought to correspond to a menu item, represented by [Action] objects. 11858 */ 11859 class ToolBar : Widget { 11860 version(win32_widgets) { 11861 private int idealHeight; 11862 override int minHeight() { return idealHeight; } 11863 override int maxHeight() { return idealHeight; } 11864 } else version(custom_widgets) { 11865 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 11866 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 11867 } else static assert(false); 11868 override int heightStretchiness() { return 0; } 11869 11870 static struct ToolbarSection { 11871 string name; 11872 Action[] actions; 11873 } 11874 11875 version(win32_widgets) { 11876 HIMAGELIST imageListSmall; 11877 HIMAGELIST imageListLarge; 11878 } 11879 11880 this(Widget parent) { 11881 this(cast(ToolbarSection[]) null, parent); 11882 } 11883 11884 version(win32_widgets) 11885 void changeIconSize(bool useLarge) { 11886 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 11887 11888 /+ 11889 SIZE size; 11890 import core.sys.windows.commctrl; 11891 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 11892 idealHeight = size.cy + 4; // the plus 4 is a hack 11893 +/ 11894 11895 idealHeight = useLarge ? 34 : 26; 11896 11897 if(parent) { 11898 parent.queueRecomputeChildLayout(); 11899 parent.redraw(); 11900 } 11901 11902 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 11903 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 11904 } 11905 11906 /++ 11907 History: 11908 The `ToolbarSection` overload was added December 31, 2024 11909 +/ 11910 this(Action[] actions, Widget parent) { 11911 this([ToolbarSection(null, actions)], parent); 11912 } 11913 11914 /// ditto 11915 this(ToolbarSection[] sections, Widget parent) { 11916 super(parent); 11917 11918 tabStop = false; 11919 11920 version(win32_widgets) { 11921 // so i like how the flat thing looks on windows, but not on wine 11922 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 11923 // leave it commented 11924 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 11925 11926 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 11927 11928 imageListSmall = ImageList_Create( 11929 // width, height 11930 16, 16, 11931 ILC_COLOR16 | ILC_MASK, 11932 16 /*numberOfButtons*/, 0); 11933 11934 imageListLarge = ImageList_Create( 11935 // width, height 11936 24, 24, 11937 ILC_COLOR16 | ILC_MASK, 11938 16 /*numberOfButtons*/, 0); 11939 11940 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 11941 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 11942 11943 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 11944 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 11945 11946 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 11947 11948 TBBUTTON[] buttons; 11949 11950 // FIXME: I_IMAGENONE is if here is no icon 11951 foreach(sidx, section; sections) { 11952 if(sidx) 11953 buttons ~= TBBUTTON( 11954 scaleWithDpi(4), 11955 0, 11956 TBSTATE_ENABLED, // state 11957 TBSTYLE_SEP | BTNS_SEP, // style 11958 0, // reserved array, just zero it out 11959 0, // dwData 11960 -1 11961 ); 11962 11963 foreach(action; section.actions) 11964 buttons ~= TBBUTTON( 11965 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 11966 action.id, 11967 TBSTATE_ENABLED, // state 11968 0, // style 11969 0, // reserved array, just zero it out 11970 0, // dwData 11971 cast(size_t) toWstringzInternal(action.label) // INT_PTR 11972 ); 11973 } 11974 11975 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 11976 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 11977 11978 /* 11979 RECT rect; 11980 GetWindowRect(hwnd, &rect); 11981 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 11982 */ 11983 11984 dpiChanged(); // to load the things calling changeIconSize the first time 11985 11986 assert(idealHeight); 11987 } else version(custom_widgets) { 11988 foreach(sidx, section; sections) { 11989 if(sidx) 11990 new HorizontalSpacer(4, this); 11991 foreach(action; section.actions) 11992 new ToolButton(action, this); 11993 } 11994 } else static assert(false); 11995 } 11996 11997 override void recomputeChildLayout() { 11998 .recomputeChildLayout!"width"(this); 11999 } 12000 12001 12002 version(win32_widgets) 12003 override protected void dpiChanged() { 12004 auto sz = scaleWithDpi(16); 12005 if(sz >= 20) 12006 changeIconSize(true); 12007 else 12008 changeIconSize(false); 12009 } 12010 } 12011 12012 /// 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. 12013 class ToolButton : Button { 12014 /// 12015 this(Action action, Widget parent) { 12016 super(action.label, parent); 12017 tabStop = false; 12018 this.action = action; 12019 } 12020 12021 version(custom_widgets) 12022 override void defaultEventHandler_click(ClickEvent event) { 12023 foreach(handler; action.triggered) 12024 handler(); 12025 } 12026 12027 Action action; 12028 12029 override int maxWidth() { return toolbarIconSize; } 12030 override int minWidth() { return toolbarIconSize; } 12031 override int maxHeight() { return toolbarIconSize; } 12032 override int minHeight() { return toolbarIconSize; } 12033 12034 version(custom_widgets) 12035 override void paint(WidgetPainter painter) { 12036 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 12037 painter.outlineColor = Color.black; 12038 12039 immutable multiplier = toolbarIconSize / 4; 12040 immutable divisor = 16 / 4; 12041 12042 int ScaledNumber(int n) { 12043 // return n * multiplier / divisor; 12044 auto s = n * multiplier; 12045 auto it = s / divisor; 12046 auto rem = s % divisor; 12047 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 12048 it++; 12049 return it; 12050 } 12051 12052 arsd.color.Point Point(int x, int y) { 12053 return arsd.color.Point(ScaledNumber(x), ScaledNumber(y)); 12054 } 12055 12056 switch(action.iconId) { 12057 case GenericIcons.New: 12058 painter.fillColor = Color.white; 12059 painter.drawPolygon( 12060 Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6), 12061 Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2), 12062 Point(3, 2), Point(3, 13) 12063 ); 12064 break; 12065 case GenericIcons.Save: 12066 painter.fillColor = Color.white; 12067 painter.outlineColor = Color.black; 12068 painter.drawRectangle(Point(2, 2), Point(13, 13)); 12069 12070 // the label 12071 painter.drawRectangle(Point(4, 8), Point(11, 13)); 12072 12073 // the slider 12074 painter.fillColor = Color.black; 12075 painter.outlineColor = Color.black; 12076 painter.drawRectangle(Point(4, 3), Point(10, 6)); 12077 12078 painter.fillColor = Color.white; 12079 painter.outlineColor = Color.white; 12080 // the disc window 12081 painter.drawRectangle(Point(5, 3), Point(6, 5)); 12082 break; 12083 case GenericIcons.Open: 12084 painter.fillColor = Color.white; 12085 painter.drawPolygon( 12086 Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3), 12087 Point(9, 3), Point(9, 4), Point(4, 4)); 12088 painter.drawPolygon( 12089 Point(2, 6), Point(11, 6), 12090 Point(12, 12), Point(4, 12), 12091 Point(2, 6)); 12092 //painter.drawLine(Point(9, 6), Point(13, 7)); 12093 break; 12094 case GenericIcons.Copy: 12095 painter.fillColor = Color.white; 12096 painter.drawRectangle(Point(3, 2), Point(9, 10)); 12097 painter.drawRectangle(Point(6, 5), Point(12, 13)); 12098 break; 12099 case GenericIcons.Cut: 12100 painter.fillColor = Color.transparent; 12101 painter.outlineColor = getComputedStyle.foregroundColor(); 12102 painter.drawLine(Point(3, 2), Point(10, 9)); 12103 painter.drawLine(Point(4, 9), Point(11, 2)); 12104 painter.drawRectangle(Point(3, 9), Point(5, 13)); 12105 painter.drawRectangle(Point(9, 9), Point(11, 12)); 12106 break; 12107 case GenericIcons.Paste: 12108 painter.fillColor = Color.white; 12109 painter.drawRectangle(Point(2, 3), Point(11, 11)); 12110 painter.drawRectangle(Point(6, 8), Point(13, 13)); 12111 painter.drawLine(Point(6, 2), Point(4, 5)); 12112 painter.drawLine(Point(6, 2), Point(9, 5)); 12113 painter.fillColor = Color.black; 12114 painter.drawRectangle(Point(4, 5), Point(9, 6)); 12115 break; 12116 case GenericIcons.Help: 12117 painter.outlineColor = getComputedStyle.foregroundColor(); 12118 painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 12119 break; 12120 case GenericIcons.Undo: 12121 painter.fillColor = Color.transparent; 12122 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 12123 painter.outlineColor = Color.black; 12124 painter.fillColor = Color.black; 12125 painter.drawPolygon( 12126 Point(4, 4), 12127 Point(8, 2), 12128 Point(8, 6), 12129 Point(4, 4), 12130 ); 12131 break; 12132 case GenericIcons.Redo: 12133 painter.fillColor = Color.transparent; 12134 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 12135 painter.outlineColor = Color.black; 12136 painter.fillColor = Color.black; 12137 painter.drawPolygon( 12138 Point(10, 4), 12139 Point(6, 2), 12140 Point(6, 6), 12141 Point(10, 4), 12142 ); 12143 break; 12144 default: 12145 painter.outlineColor = getComputedStyle.foregroundColor; 12146 painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 12147 } 12148 return bounds; 12149 }); 12150 } 12151 12152 } 12153 12154 12155 /++ 12156 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 12157 +/ 12158 class MenuBar : Widget { 12159 MenuItem[] items; 12160 Menu[] subMenus; 12161 12162 version(win32_widgets) { 12163 HMENU handle; 12164 /// 12165 this(Widget parent = null) { 12166 super(parent); 12167 12168 handle = CreateMenu(); 12169 tabStop = false; 12170 } 12171 } else version(custom_widgets) { 12172 /// 12173 this(Widget parent = null) { 12174 tabStop = false; // these are selected some other way 12175 super(parent); 12176 } 12177 12178 mixin Padding!q{2}; 12179 } else static assert(false); 12180 12181 version(custom_widgets) 12182 override void paint(WidgetPainter painter) { 12183 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 12184 } 12185 12186 /// 12187 MenuItem addItem(MenuItem item) { 12188 this.addChild(item); 12189 items ~= item; 12190 version(win32_widgets) { 12191 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 12192 } 12193 return item; 12194 } 12195 12196 12197 /// 12198 Menu addItem(Menu item) { 12199 12200 subMenus ~= item; 12201 12202 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 12203 12204 addChild(mbItem); 12205 items ~= mbItem; 12206 12207 version(win32_widgets) { 12208 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 12209 } else version(custom_widgets) { 12210 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 12211 item.popup(mbItem); 12212 }; 12213 } else static assert(false); 12214 12215 return item; 12216 } 12217 12218 override void recomputeChildLayout() { 12219 .recomputeChildLayout!"width"(this); 12220 } 12221 12222 override int maxHeight() { return defaultLineHeight + 4; } 12223 override int minHeight() { return defaultLineHeight + 4; } 12224 } 12225 12226 12227 /** 12228 Status bars appear at the bottom of a MainWindow. 12229 They are made out of Parts, with a width and content. 12230 12231 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 12232 12233 12234 sb.parts[0].content = "Status bar text!"; 12235 */ 12236 // https://learn.microsoft.com/en-us/windows/win32/controls/status-bars#owner-drawn-status-bars 12237 class StatusBar : Widget { 12238 private Part[] partsArray; 12239 /// 12240 struct Parts { 12241 @disable this(); 12242 this(StatusBar owner) { this.owner = owner; } 12243 //@disable this(this); 12244 /// 12245 @property int length() { return cast(int) owner.partsArray.length; } 12246 private StatusBar owner; 12247 private this(StatusBar owner, Part[] parts) { 12248 this.owner.partsArray = parts; 12249 this.owner = owner; 12250 } 12251 /// 12252 Part opIndex(int p) { 12253 if(owner.partsArray.length == 0) 12254 this ~= new StatusBar.Part(0); 12255 return owner.partsArray[p]; 12256 } 12257 12258 /// 12259 Part opOpAssign(string op : "~" )(Part p) { 12260 assert(owner.partsArray.length < 255); 12261 p.owner = this.owner; 12262 p.idx = cast(int) owner.partsArray.length; 12263 owner.partsArray ~= p; 12264 12265 owner.queueRecomputeChildLayout(); 12266 12267 version(win32_widgets) { 12268 int[256] pos; 12269 int cpos; 12270 foreach(idx, part; owner.partsArray) { 12271 if(idx + 1 == owner.partsArray.length) 12272 pos[idx] = -1; 12273 else { 12274 cpos += part.currentlyAssignedWidth; 12275 pos[idx] = cpos; 12276 } 12277 } 12278 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 12279 } else version(custom_widgets) { 12280 owner.redraw(); 12281 } else static assert(false); 12282 12283 return p; 12284 } 12285 12286 /++ 12287 Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions. 12288 12289 No given item should be 0. 12290 12291 History: 12292 Added December 31, 2024 12293 +/ 12294 void setSizes(int[] proportions...) { 12295 assert(this.owner); 12296 this.owner.partsArray = null; 12297 12298 foreach(n; proportions) { 12299 assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead."); 12300 12301 this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels)); 12302 } 12303 12304 } 12305 } 12306 12307 private Parts _parts; 12308 /// 12309 final @property Parts parts() { 12310 return _parts; 12311 } 12312 12313 /++ 12314 12315 +/ 12316 static class Part { 12317 /++ 12318 History: 12319 Added September 1, 2023 (dub v11.1) 12320 +/ 12321 enum WidthUnits { 12322 /++ 12323 Unscaled pixels as they appear on screen. 12324 12325 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 12326 +/ 12327 DeviceDependentPixels, 12328 /++ 12329 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 12330 +/ 12331 DeviceIndependentPixels, 12332 /++ 12333 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`). 12334 +/ 12335 ApproximateCharacters, 12336 /++ 12337 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. 12338 12339 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. 12340 +/ 12341 Proportional 12342 } 12343 private WidthUnits units; 12344 private int width; 12345 private StatusBar owner; 12346 12347 private int currentlyAssignedWidth; 12348 12349 /++ 12350 History: 12351 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. 12352 12353 It now allows you to provide your own value for [WidthUnits]. 12354 12355 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`. 12356 +/ 12357 this(int w, WidthUnits units = WidthUnits.Proportional) { 12358 this.units = units; 12359 this.width = w; 12360 } 12361 12362 /// ditto 12363 this(int w = 0) { 12364 if(w == 0) 12365 this(w, WidthUnits.Proportional); 12366 else 12367 this(w, WidthUnits.DeviceDependentPixels); 12368 } 12369 12370 private int idx; 12371 private string _content; 12372 /// 12373 @property string content() { return _content; } 12374 /// 12375 @property void content(string s) { 12376 version(win32_widgets) { 12377 _content = s; 12378 WCharzBuffer bfr = WCharzBuffer(s); 12379 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 12380 } else version(custom_widgets) { 12381 if(_content != s) { 12382 _content = s; 12383 owner.redraw(); 12384 } 12385 } else static assert(false); 12386 } 12387 } 12388 string simpleModeContent; 12389 bool inSimpleMode; 12390 12391 12392 /// 12393 this(Widget parent) { 12394 super(null); // FIXME 12395 _parts = Parts(this); 12396 tabStop = false; 12397 version(win32_widgets) { 12398 parentWindow = parent.parentWindow; 12399 createWin32Window(this, "msctls_statusbar32"w, "", 0); 12400 12401 RECT rect; 12402 GetWindowRect(hwnd, &rect); 12403 idealHeight = rect.bottom - rect.top; 12404 assert(idealHeight); 12405 } else version(custom_widgets) { 12406 } else static assert(false); 12407 } 12408 12409 override void recomputeChildLayout() { 12410 int remainingLength = this.width; 12411 12412 int proportionalSum; 12413 int proportionalCount; 12414 foreach(idx, part; this.partsArray) { 12415 with(Part.WidthUnits) 12416 final switch(part.units) { 12417 case DeviceDependentPixels: 12418 part.currentlyAssignedWidth = part.width; 12419 remainingLength -= part.currentlyAssignedWidth; 12420 break; 12421 case DeviceIndependentPixels: 12422 part.currentlyAssignedWidth = scaleWithDpi(part.width); 12423 remainingLength -= part.currentlyAssignedWidth; 12424 break; 12425 case ApproximateCharacters: 12426 auto cs = getComputedStyle(); 12427 auto font = cs.font; 12428 12429 part.currentlyAssignedWidth = castFnumToCnum(font.averageWidth * this.width); 12430 remainingLength -= part.currentlyAssignedWidth; 12431 break; 12432 case Proportional: 12433 proportionalSum += part.width; 12434 proportionalCount ++; 12435 break; 12436 } 12437 } 12438 12439 foreach(part; this.partsArray) { 12440 if(part.units == Part.WidthUnits.Proportional) { 12441 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 12442 if(proportion == 0) 12443 proportion = 1; 12444 12445 if(proportionalSum == 0) 12446 proportionalSum = proportionalCount; 12447 12448 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 12449 } 12450 } 12451 12452 super.recomputeChildLayout(); 12453 } 12454 12455 version(win32_widgets) 12456 override protected void dpiChanged() { 12457 RECT rect; 12458 GetWindowRect(hwnd, &rect); 12459 idealHeight = rect.bottom - rect.top; 12460 assert(idealHeight); 12461 } 12462 12463 version(custom_widgets) 12464 override void paint(WidgetPainter painter) { 12465 auto cs = getComputedStyle(); 12466 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12467 int cpos = 0; 12468 foreach(idx, part; this.partsArray) { 12469 auto partWidth = part.currentlyAssignedWidth; 12470 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 12471 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 12472 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 12473 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 12474 12475 painter.outlineColor = cs.foregroundColor(); 12476 painter.fillColor = cs.foregroundColor(); 12477 12478 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 12479 cpos += partWidth; 12480 } 12481 } 12482 12483 12484 version(win32_widgets) { 12485 private int idealHeight; 12486 override int maxHeight() { return idealHeight; } 12487 override int minHeight() { return idealHeight; } 12488 } else version(custom_widgets) { 12489 override int maxHeight() { return defaultLineHeight + 4; } 12490 override int minHeight() { return defaultLineHeight + 4; } 12491 } else static assert(false); 12492 } 12493 12494 /// Displays an in-progress indicator without known values 12495 version(none) 12496 class IndefiniteProgressBar : Widget { 12497 version(win32_widgets) 12498 this(Widget parent) { 12499 super(parent); 12500 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 12501 tabStop = false; 12502 } 12503 override int minHeight() { return 10; } 12504 } 12505 12506 /// A progress bar with a known endpoint and completion amount 12507 class ProgressBar : Widget { 12508 /++ 12509 History: 12510 Added March 16, 2022 (dub v10.7) 12511 +/ 12512 this(int min, int max, Widget parent) { 12513 this(parent); 12514 setRange(cast(ushort) min, cast(ushort) max); // FIXME 12515 } 12516 this(Widget parent) { 12517 version(win32_widgets) { 12518 super(parent); 12519 createWin32Window(this, "msctls_progress32"w, "", 0); 12520 tabStop = false; 12521 } else version(custom_widgets) { 12522 super(parent); 12523 max = 100; 12524 step = 10; 12525 tabStop = false; 12526 } else static assert(0); 12527 } 12528 12529 version(custom_widgets) 12530 override void paint(WidgetPainter painter) { 12531 auto cs = getComputedStyle(); 12532 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12533 painter.fillColor = cs.progressBarColor; 12534 painter.drawRectangle(Point(0, 0), width * current / max, height); 12535 } 12536 12537 12538 version(custom_widgets) { 12539 int current; 12540 int max; 12541 int step; 12542 } 12543 12544 /// 12545 void advanceOneStep() { 12546 version(win32_widgets) 12547 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 12548 else version(custom_widgets) 12549 addToPosition(step); 12550 else static assert(false); 12551 } 12552 12553 /// 12554 void setStepIncrement(int increment) { 12555 version(win32_widgets) 12556 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 12557 else version(custom_widgets) 12558 step = increment; 12559 else static assert(false); 12560 } 12561 12562 /// 12563 void addToPosition(int amount) { 12564 version(win32_widgets) 12565 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 12566 else version(custom_widgets) 12567 setPosition(current + amount); 12568 else static assert(false); 12569 } 12570 12571 /// 12572 void setPosition(int pos) { 12573 version(win32_widgets) 12574 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 12575 else version(custom_widgets) { 12576 current = pos; 12577 if(current > max) 12578 current = max; 12579 redraw(); 12580 } 12581 else static assert(false); 12582 } 12583 12584 /// 12585 void setRange(ushort min, ushort max) { 12586 version(win32_widgets) 12587 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 12588 else version(custom_widgets) { 12589 this.max = max; 12590 } 12591 else static assert(false); 12592 } 12593 12594 override int minHeight() { return 10; } 12595 } 12596 12597 version(custom_widgets) 12598 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 12599 thisLabel.reserve(label.length); 12600 bool justSawAmpersand; 12601 foreach(ch; label) { 12602 if(justSawAmpersand) { 12603 justSawAmpersand = false; 12604 if(ch == '&') { 12605 goto plain; 12606 } 12607 thisAccelerator = ch; 12608 } else { 12609 if(ch == '&') { 12610 justSawAmpersand = true; 12611 continue; 12612 } 12613 plain: 12614 thisLabel ~= ch; 12615 } 12616 } 12617 } 12618 12619 /++ 12620 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. 12621 12622 12623 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 12624 12625 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12626 12627 History: 12628 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. 12629 +/ 12630 class Fieldset : Widget { 12631 // FIXME: on Windows,it doesn't draw the background on the label 12632 // on X, it doesn't fix the clipping rectangle for it 12633 version(win32_widgets) 12634 override int paddingTop() { return defaultLineHeight; } 12635 else version(custom_widgets) 12636 override int paddingTop() { return defaultLineHeight + 2; } 12637 else static assert(false); 12638 override int paddingBottom() { return 6; } 12639 override int paddingLeft() { return 6; } 12640 override int paddingRight() { return 6; } 12641 12642 override int marginLeft() { return 6; } 12643 override int marginRight() { return 6; } 12644 override int marginTop() { return 2; } 12645 override int marginBottom() { return 2; } 12646 12647 string legend; 12648 12649 version(custom_widgets) private dchar accelerator; 12650 12651 this(string legend, Widget parent) { 12652 version(win32_widgets) { 12653 super(parent); 12654 this.legend = legend; 12655 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 12656 tabStop = false; 12657 } else version(custom_widgets) { 12658 super(parent); 12659 tabStop = false; 12660 12661 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 12662 } else static assert(0); 12663 } 12664 12665 version(custom_widgets) 12666 override void paint(WidgetPainter painter) { 12667 auto dlh = defaultLineHeight; 12668 12669 painter.fillColor = Color.transparent; 12670 auto cs = getComputedStyle(); 12671 painter.pen = Pen(cs.foregroundColor, 1); 12672 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 12673 12674 auto tx = painter.textSize(legend); 12675 painter.outlineColor = Color.transparent; 12676 12677 version(Windows) { 12678 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 12679 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 12680 SelectObject(painter.impl.hdc, b); 12681 } else static if(UsingSimpledisplayX11) { 12682 painter.fillColor = getComputedStyle().windowBackgroundColor; 12683 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 12684 } 12685 painter.outlineColor = cs.foregroundColor; 12686 painter.drawText(Point(8, 0), legend); 12687 } 12688 12689 override int maxHeight() { 12690 auto m = paddingTop() + paddingBottom(); 12691 foreach(child; children) { 12692 auto mh = child.maxHeight(); 12693 if(mh == int.max) 12694 return int.max; 12695 m += mh; 12696 m += child.marginBottom(); 12697 m += child.marginTop(); 12698 } 12699 m += 6; 12700 if(m < minHeight) 12701 return minHeight; 12702 return m; 12703 } 12704 12705 override int minHeight() { 12706 auto m = paddingTop() + paddingBottom(); 12707 foreach(child; children) { 12708 m += child.minHeight(); 12709 m += child.marginBottom(); 12710 m += child.marginTop(); 12711 } 12712 return m + 6; 12713 } 12714 12715 override int minWidth() { 12716 return 6 + cast(int) this.legend.length * 7; 12717 } 12718 } 12719 12720 /++ 12721 $(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") 12722 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 12723 +/ 12724 version(minigui_screenshots) 12725 @Screenshot("Fieldset") 12726 unittest { 12727 auto window = new Window(200, 100); 12728 auto set = new Fieldset("Baby will", window); 12729 auto option1 = new Radiobox("Eat", set); 12730 auto option2 = new Radiobox("Cry", set); 12731 auto option3 = new Radiobox("Sleep", set); 12732 window.loop(); 12733 } 12734 12735 /// Draws a line 12736 class HorizontalRule : Widget { 12737 mixin Margin!q{ 2 }; 12738 override int minHeight() { return 2; } 12739 override int maxHeight() { return 2; } 12740 12741 /// 12742 this(Widget parent) { 12743 super(parent); 12744 } 12745 12746 override void paint(WidgetPainter painter) { 12747 auto cs = getComputedStyle(); 12748 painter.outlineColor = cs.darkAccentColor; 12749 painter.drawLine(Point(0, 0), Point(width, 0)); 12750 painter.outlineColor = cs.lightAccentColor; 12751 painter.drawLine(Point(0, 1), Point(width, 1)); 12752 } 12753 } 12754 12755 version(minigui_screenshots) 12756 @Screenshot("HorizontalRule") 12757 /++ 12758 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 12759 12760 +/ 12761 unittest { 12762 auto window = new Window(200, 100); 12763 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 12764 new HorizontalRule(window); 12765 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 12766 window.loop(); 12767 } 12768 12769 /// ditto 12770 class VerticalRule : Widget { 12771 mixin Margin!q{ 2 }; 12772 override int minWidth() { return 2; } 12773 override int maxWidth() { return 2; } 12774 12775 /// 12776 this(Widget parent) { 12777 super(parent); 12778 } 12779 12780 override void paint(WidgetPainter painter) { 12781 auto cs = getComputedStyle(); 12782 painter.outlineColor = cs.darkAccentColor; 12783 painter.drawLine(Point(0, 0), Point(0, height)); 12784 painter.outlineColor = cs.lightAccentColor; 12785 painter.drawLine(Point(1, 0), Point(1, height)); 12786 } 12787 } 12788 12789 12790 /// 12791 class Menu : Window { 12792 void remove() { 12793 foreach(i, child; parentWindow.children) 12794 if(child is this) { 12795 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 12796 break; 12797 } 12798 parentWindow.redraw(); 12799 12800 parentWindow.releaseMouseCapture(); 12801 } 12802 12803 /// 12804 void addSeparator() { 12805 version(win32_widgets) 12806 AppendMenu(handle, MF_SEPARATOR, 0, null); 12807 else version(custom_widgets) 12808 auto hr = new HorizontalRule(this); 12809 else static assert(0); 12810 } 12811 12812 override int paddingTop() { return 4; } 12813 override int paddingBottom() { return 4; } 12814 override int paddingLeft() { return 2; } 12815 override int paddingRight() { return 2; } 12816 12817 version(win32_widgets) {} 12818 else version(custom_widgets) { 12819 12820 Widget previouslyFocusedWidget; 12821 Widget* previouslyFocusedWidgetBelongsIn; 12822 12823 SimpleWindow dropDown; 12824 Widget menuParent; 12825 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 12826 this.menuParent = parent; 12827 12828 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 12829 previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget; 12830 parent.parentWindow.focusedWidget = this; 12831 12832 int w = 150; 12833 int h = paddingTop + paddingBottom; 12834 if(this.children.length) { 12835 // hacking it to get the ideal height out of recomputeChildLayout 12836 this.width = w; 12837 this.height = h; 12838 this.recomputeChildLayoutEntry(); 12839 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 12840 h += paddingBottom; 12841 12842 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 12843 } 12844 12845 if(offsetY == int.min) 12846 offsetY = parent.defaultLineHeight; 12847 12848 auto coord = parent.globalCoordinates(); 12849 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 12850 this.x = 0; 12851 this.y = 0; 12852 this.width = dropDown.width; 12853 this.height = dropDown.height; 12854 this.drawableWindow = dropDown; 12855 this.recomputeChildLayoutEntry(); 12856 12857 static if(UsingSimpledisplayX11) 12858 XSync(XDisplayConnection.get, 0); 12859 12860 dropDown.visibilityChanged = (bool visible) { 12861 if(visible) { 12862 this.redraw(); 12863 dropDown.grabInput(); 12864 } else { 12865 dropDown.releaseInputGrab(); 12866 } 12867 }; 12868 12869 dropDown.show(); 12870 12871 clickListener = this.addEventListener((scope ClickEvent ev) { 12872 unpopup(); 12873 // need to unlock asap just in case other user handlers block... 12874 static if(UsingSimpledisplayX11) 12875 flushGui(); 12876 }, true /* again for asap action */); 12877 } 12878 12879 EventListener clickListener; 12880 } 12881 else static assert(false); 12882 12883 version(custom_widgets) 12884 void unpopup() { 12885 mouseLastOver = mouseLastDownOn = null; 12886 dropDown.hide(); 12887 if(!menuParent.parentWindow.win.closed) { 12888 if(auto maw = cast(MouseActivatedWidget) menuParent) { 12889 maw.setDynamicState(DynamicState.depressed, false); 12890 maw.setDynamicState(DynamicState.hover, false); 12891 maw.redraw(); 12892 } 12893 // menuParent.parentWindow.win.focus(); 12894 } 12895 clickListener.disconnect(); 12896 12897 if(previouslyFocusedWidgetBelongsIn) 12898 *previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget; 12899 } 12900 12901 MenuItem[] items; 12902 12903 /// 12904 MenuItem addItem(MenuItem item) { 12905 addChild(item); 12906 items ~= item; 12907 version(win32_widgets) { 12908 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 12909 } 12910 return item; 12911 } 12912 12913 string label; 12914 12915 version(win32_widgets) { 12916 HMENU handle; 12917 /// 12918 this(string label, Widget parent) { 12919 // not actually passing the parent since it effs up the drawing 12920 super(cast(Widget) null);// parent); 12921 this.label = label; 12922 handle = CreatePopupMenu(); 12923 } 12924 } else version(custom_widgets) { 12925 /// 12926 this(string label, Widget parent) { 12927 12928 if(dropDown) { 12929 dropDown.close(); 12930 } 12931 dropDown = new SimpleWindow( 12932 150, 4, 12933 // FIXME: what if it is a popupMenu ? 12934 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 12935 12936 this.label = label; 12937 12938 super(dropDown); 12939 } 12940 } else static assert(false); 12941 12942 override int maxHeight() { return defaultLineHeight; } 12943 override int minHeight() { return defaultLineHeight; } 12944 12945 version(custom_widgets) { 12946 Widget currentPlace; 12947 12948 void changeCurrentPlace(Widget n) { 12949 if(currentPlace) { 12950 currentPlace.dynamicState = 0; 12951 } 12952 12953 if(n) { 12954 n.dynamicState = DynamicState.hover; 12955 } 12956 12957 currentPlace = n; 12958 } 12959 12960 override void paint(WidgetPainter painter) { 12961 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 12962 } 12963 12964 override void defaultEventHandler_keydown(KeyDownEvent ke) { 12965 switch(ke.key) { 12966 case Key.Down: 12967 Widget next; 12968 Widget first; 12969 foreach(w; this.children) { 12970 if((cast(MenuItem) w) is null) 12971 continue; 12972 12973 if(first is null) 12974 first = w; 12975 12976 if(next !is null) { 12977 next = w; 12978 break; 12979 } 12980 12981 if(currentPlace is null) { 12982 next = w; 12983 break; 12984 } 12985 12986 if(w is currentPlace) { 12987 next = w; 12988 } 12989 } 12990 12991 if(next is currentPlace) 12992 next = first; 12993 12994 changeCurrentPlace(next); 12995 break; 12996 case Key.Up: 12997 Widget prev; 12998 foreach(w; this.children) { 12999 if((cast(MenuItem) w) is null) 13000 continue; 13001 if(w is currentPlace) { 13002 if(prev is null) { 13003 foreach_reverse(c; this.children) { 13004 if((cast(MenuItem) c) !is null) { 13005 prev = c; 13006 break; 13007 } 13008 } 13009 } 13010 break; 13011 } 13012 prev = w; 13013 } 13014 changeCurrentPlace(prev); 13015 break; 13016 case Key.Left: 13017 case Key.Right: 13018 if(menuParent) { 13019 Menu first; 13020 Menu last; 13021 Menu prev; 13022 Menu next; 13023 bool found; 13024 13025 size_t prev_idx; 13026 size_t next_idx; 13027 13028 MenuBar mb = cast(MenuBar) menuParent.parent; 13029 13030 if(mb) { 13031 foreach(idx, menu; mb.subMenus) { 13032 if(first is null) 13033 first = menu; 13034 last = menu; 13035 if(found && next is null) { 13036 next = menu; 13037 next_idx = idx; 13038 } 13039 if(menu is this) 13040 found = true; 13041 if(!found) { 13042 prev = menu; 13043 prev_idx = idx; 13044 } 13045 } 13046 13047 Menu nextMenu; 13048 size_t nextMenuIdx; 13049 if(ke.key == Key.Left) { 13050 nextMenu = prev ? prev : last; 13051 nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1; 13052 } else { 13053 nextMenu = next ? next : first; 13054 nextMenuIdx = next ? next_idx : 0; 13055 } 13056 13057 unpopup(); 13058 13059 auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right 13060 rent.dynamicState = DynamicState.depressed | DynamicState.hover; 13061 nextMenu.popup(rent); 13062 } 13063 } 13064 break; 13065 case Key.Enter: 13066 case Key.PadEnter: 13067 // because the key up and char events will go back to the other window after we unpopup! 13068 // we will wait for the char event to come (in the following method) 13069 break; 13070 case Key.Escape: 13071 unpopup(); 13072 break; 13073 default: 13074 } 13075 } 13076 override void defaultEventHandler_char(CharEvent ke) { 13077 // if one is selected, enter activates it 13078 if(currentPlace) { 13079 if(ke.character == '\n') { 13080 // enter selects 13081 auto event = new Event(EventType.triggered, currentPlace); 13082 event.dispatch(); 13083 unpopup(); 13084 return; 13085 } 13086 } 13087 13088 // otherwise search for a hotkey 13089 foreach(item; items) { 13090 if(item.hotkey == ke.character) { 13091 auto event = new Event(EventType.triggered, item); 13092 event.dispatch(); 13093 unpopup(); 13094 return; 13095 } 13096 } 13097 } 13098 override void defaultEventHandler_mouseover(MouseOverEvent moe) { 13099 if(moe.target && moe.target.parent is this) 13100 changeCurrentPlace(moe.target); 13101 } 13102 } 13103 } 13104 13105 /++ 13106 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 13107 +/ 13108 class MenuItem : MouseActivatedWidget { 13109 Menu submenu; 13110 13111 Action action; 13112 string label; 13113 dchar hotkey; 13114 13115 override int paddingLeft() { return 4; } 13116 13117 override int maxHeight() { return defaultLineHeight + 4; } 13118 override int minHeight() { return defaultLineHeight + 4; } 13119 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 13120 override int maxWidth() { 13121 if(cast(MenuBar) parent) { 13122 return minWidth(); 13123 } 13124 return int.max; 13125 } 13126 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 13127 this(string lbl, Widget parent = null) { 13128 super(parent); 13129 //label = lbl; // FIXME 13130 foreach(idx, char ch; lbl) // FIXME 13131 if(ch != '&') { // FIXME 13132 label ~= ch; // FIXME 13133 } else { 13134 if(idx + 1 < lbl.length) { 13135 hotkey = lbl[idx + 1]; 13136 if(hotkey >= 'A' && hotkey <= 'Z') 13137 hotkey += 32; 13138 } 13139 } 13140 tabStop = false; // these are selected some other way 13141 } 13142 13143 /// 13144 this(Action action, Widget parent = null) { 13145 assert(action !is null); 13146 this(action.label, parent); 13147 this.action = action; 13148 tabStop = false; // these are selected some other way 13149 } 13150 13151 version(custom_widgets) 13152 override void paint(WidgetPainter painter) { 13153 auto cs = getComputedStyle(); 13154 if(dynamicState & DynamicState.depressed) 13155 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 13156 else { 13157 if(dynamicState & DynamicState.hover) { 13158 painter.fillColor = cs.hoveringColor; 13159 painter.outlineColor = Color.transparent; 13160 } else { 13161 painter.fillColor = cs.background.color; 13162 painter.outlineColor = Color.transparent; 13163 } 13164 13165 painter.drawRectangle(Point(0, 0), Size(this.width, this.height)); 13166 } 13167 13168 if(dynamicState & DynamicState.hover) 13169 painter.outlineColor = cs.activeMenuItemColor; 13170 else 13171 painter.outlineColor = cs.foregroundColor; 13172 painter.fillColor = Color.transparent; 13173 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 13174 if(action && action.accelerator !is KeyEvent.init) { 13175 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 13176 13177 } 13178 } 13179 13180 static class Style : Widget.Style { 13181 override bool variesWithState(ulong dynamicStateFlags) { 13182 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 13183 } 13184 } 13185 mixin OverrideStyle!Style; 13186 13187 override void defaultEventHandler_triggered(Event event) { 13188 if(action) 13189 foreach(handler; action.triggered) 13190 handler(); 13191 13192 if(auto pmenu = cast(Menu) this.parent) 13193 pmenu.remove(); 13194 13195 super.defaultEventHandler_triggered(event); 13196 } 13197 } 13198 13199 version(win32_widgets) 13200 /// A "mouse activiated widget" is really just an abstract variant of button. 13201 class MouseActivatedWidget : Widget { 13202 @property bool isChecked() { 13203 assert(hwnd); 13204 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 13205 13206 } 13207 @property void isChecked(bool state) { 13208 assert(hwnd); 13209 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 13210 13211 } 13212 13213 override void handleWmCommand(ushort cmd, ushort id) { 13214 if(cmd == 0) { 13215 auto event = new Event(EventType.triggered, this); 13216 event.dispatch(); 13217 } 13218 } 13219 13220 this(Widget parent) { 13221 super(parent); 13222 } 13223 } 13224 else version(custom_widgets) 13225 /// ditto 13226 class MouseActivatedWidget : Widget { 13227 @property bool isChecked() { return isChecked_; } 13228 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 13229 13230 private bool isChecked_; 13231 13232 this(Widget parent) { 13233 super(parent); 13234 13235 addEventListener((MouseDownEvent ev) { 13236 if(ev.button == MouseButton.left) { 13237 setDynamicState(DynamicState.depressed, true); 13238 setDynamicState(DynamicState.hover, true); 13239 redraw(); 13240 } 13241 }); 13242 13243 addEventListener((MouseUpEvent ev) { 13244 if(ev.button == MouseButton.left) { 13245 setDynamicState(DynamicState.depressed, false); 13246 setDynamicState(DynamicState.hover, false); 13247 redraw(); 13248 } 13249 }); 13250 13251 addEventListener((MouseMoveEvent mme) { 13252 if(!(mme.state & ModifierState.leftButtonDown)) { 13253 if(dynamicState_ & DynamicState.depressed) { 13254 setDynamicState(DynamicState.depressed, false); 13255 redraw(); 13256 } 13257 } 13258 }); 13259 } 13260 13261 override void defaultEventHandler_focus(FocusEvent ev) { 13262 super.defaultEventHandler_focus(ev); 13263 this.redraw(); 13264 } 13265 override void defaultEventHandler_blur(BlurEvent ev) { 13266 super.defaultEventHandler_blur(ev); 13267 setDynamicState(DynamicState.depressed, false); 13268 this.redraw(); 13269 } 13270 override void defaultEventHandler_keydown(KeyDownEvent ev) { 13271 super.defaultEventHandler_keydown(ev); 13272 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 13273 setDynamicState(DynamicState.depressed, true); 13274 setDynamicState(DynamicState.hover, true); 13275 this.redraw(); 13276 } 13277 } 13278 override void defaultEventHandler_keyup(KeyUpEvent ev) { 13279 super.defaultEventHandler_keyup(ev); 13280 if(!(dynamicState & DynamicState.depressed)) 13281 return; 13282 if(!enabled) 13283 return; 13284 setDynamicState(DynamicState.depressed, false); 13285 setDynamicState(DynamicState.hover, false); 13286 this.redraw(); 13287 13288 auto event = new Event(EventType.triggered, this); 13289 event.sendDirectly(); 13290 } 13291 override void defaultEventHandler_click(ClickEvent ev) { 13292 super.defaultEventHandler_click(ev); 13293 if(ev.button == MouseButton.left && enabled) { 13294 auto event = new Event(EventType.triggered, this); 13295 event.sendDirectly(); 13296 } 13297 } 13298 13299 } 13300 else static assert(false); 13301 13302 /* 13303 /++ 13304 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 13305 13306 Basically the same as a checkbox. 13307 +/ 13308 class OnOffSwitch : MouseActivatedWidget { 13309 13310 } 13311 */ 13312 13313 /++ 13314 History: 13315 Added June 15, 2021 (dub v10.1) 13316 +/ 13317 struct ImageLabel { 13318 /++ 13319 Defines a label+image combo used by some widgets. 13320 13321 If you provide just a text label, that is all the widget will try to 13322 display. Or just an image will display just that. If you provide both, 13323 it may display both text and image side by side or display the image 13324 and offer text on an input event depending on the widget. 13325 13326 History: 13327 The `alignment` parameter was added on September 27, 2021 13328 +/ 13329 this(string label, TextAlignment alignment = TextAlignment.Center) { 13330 this.label = label; 13331 this.displayFlags = DisplayFlags.displayText; 13332 this.alignment = alignment; 13333 } 13334 13335 /// ditto 13336 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 13337 this.label = label; 13338 this.image = image; 13339 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 13340 this.alignment = alignment; 13341 } 13342 13343 /// ditto 13344 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 13345 this.image = image; 13346 this.displayFlags = DisplayFlags.displayImage; 13347 this.alignment = alignment; 13348 } 13349 13350 /// ditto 13351 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 13352 this.label = label; 13353 this.image = image; 13354 this.alignment = alignment; 13355 this.displayFlags = displayFlags; 13356 } 13357 13358 string label; 13359 MemoryImage image; 13360 13361 enum DisplayFlags { 13362 displayText = 1 << 0, 13363 displayImage = 1 << 1, 13364 } 13365 13366 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 13367 13368 TextAlignment alignment; 13369 } 13370 13371 /++ 13372 A basic checked or not checked box with an attached label. 13373 13374 13375 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 13376 13377 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13378 13379 History: 13380 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. 13381 +/ 13382 class Checkbox : MouseActivatedWidget { 13383 version(win32_widgets) { 13384 override int maxHeight() { return scaleWithDpi(16); } 13385 override int minHeight() { return scaleWithDpi(16); } 13386 } else version(custom_widgets) { 13387 private enum buttonSize = 16; 13388 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 13389 override int minHeight() { return maxHeight(); } 13390 } else static assert(0); 13391 13392 override int marginLeft() { return 4; } 13393 13394 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 13395 13396 /++ 13397 Just an alias because I keep typing checked out of web habit. 13398 13399 History: 13400 Added May 31, 2021 13401 +/ 13402 alias checked = isChecked; 13403 13404 private string label; 13405 private dchar accelerator; 13406 13407 /++ 13408 +/ 13409 this(string label, Widget parent) { 13410 this(ImageLabel(label), Appearance.checkbox, parent); 13411 } 13412 13413 /// ditto 13414 this(string label, Appearance appearance, Widget parent) { 13415 this(ImageLabel(label), appearance, parent); 13416 } 13417 13418 /++ 13419 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 13420 13421 History: 13422 Added June 29, 2021 (dub v10.2) 13423 +/ 13424 enum Appearance { 13425 checkbox, /// a normal checkbox 13426 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 13427 //sliderswitch, 13428 } 13429 private Appearance appearance; 13430 13431 /// ditto 13432 private this(ImageLabel label, Appearance appearance, Widget parent) { 13433 super(parent); 13434 version(win32_widgets) { 13435 this.label = label.label; 13436 13437 uint extraStyle; 13438 final switch(appearance) { 13439 case Appearance.checkbox: 13440 break; 13441 case Appearance.pushbutton: 13442 extraStyle |= BS_PUSHLIKE; 13443 break; 13444 } 13445 13446 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 13447 } else version(custom_widgets) { 13448 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 13449 } else static assert(0); 13450 } 13451 13452 version(custom_widgets) 13453 override void paint(WidgetPainter painter) { 13454 auto cs = getComputedStyle(); 13455 if(isFocused()) { 13456 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 13457 painter.fillColor = cs.windowBackgroundColor; 13458 painter.drawRectangle(Point(0, 0), width, height); 13459 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 13460 } else { 13461 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 13462 painter.fillColor = cs.windowBackgroundColor; 13463 painter.drawRectangle(Point(0, 0), width, height); 13464 } 13465 13466 13467 painter.outlineColor = Color.black; 13468 painter.fillColor = Color.white; 13469 enum rectOffset = 2; 13470 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 13471 13472 if(isChecked) { 13473 auto size = scaleWithDpi(2); 13474 painter.pen = Pen(Color.black, size); 13475 // I'm using height so the checkbox is square 13476 enum padding = 3; 13477 painter.drawLine( 13478 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 13479 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 13480 ); 13481 painter.drawLine( 13482 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 13483 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 13484 ); 13485 13486 painter.pen = Pen(Color.black, 1); 13487 } 13488 13489 if(label !is null) { 13490 painter.outlineColor = cs.foregroundColor(); 13491 painter.fillColor = cs.foregroundColor(); 13492 13493 // i want the centerline of the text to be aligned with the centerline of the checkbox 13494 /+ 13495 auto font = cs.font(); 13496 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 13497 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 13498 +/ 13499 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 13500 } 13501 } 13502 13503 override void defaultEventHandler_triggered(Event ev) { 13504 isChecked = !isChecked; 13505 13506 this.emit!(ChangeEvent!bool)(&isChecked); 13507 13508 redraw(); 13509 } 13510 13511 /// Emits a change event with the checked state 13512 mixin Emits!(ChangeEvent!bool); 13513 } 13514 13515 /// Adds empty space to a layout. 13516 class VerticalSpacer : Widget { 13517 private int mh; 13518 13519 /++ 13520 History: 13521 The overload with `maxHeight` was added on December 31, 2024 13522 +/ 13523 this(Widget parent) { 13524 this(0, parent); 13525 } 13526 13527 /// ditto 13528 this(int maxHeight, Widget parent) { 13529 this.mh = maxHeight; 13530 super(parent); 13531 this.tabStop = false; 13532 } 13533 13534 override int maxHeight() { 13535 return mh ? scaleWithDpi(mh) : super.maxHeight(); 13536 } 13537 } 13538 13539 13540 /// ditto 13541 class HorizontalSpacer : Widget { 13542 private int mw; 13543 13544 /++ 13545 History: 13546 The overload with `maxWidth` was added on December 31, 2024 13547 +/ 13548 this(Widget parent) { 13549 this(0, parent); 13550 } 13551 13552 /// ditto 13553 this(int maxWidth, Widget parent) { 13554 this.mw = maxWidth; 13555 super(parent); 13556 this.tabStop = false; 13557 } 13558 13559 override int maxWidth() { 13560 return mw ? scaleWithDpi(mw) : super.maxWidth(); 13561 } 13562 } 13563 13564 13565 /++ 13566 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 13567 13568 13569 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 13570 13571 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13572 13573 History: 13574 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. 13575 +/ 13576 class Radiobox : MouseActivatedWidget { 13577 13578 version(win32_widgets) { 13579 override int maxHeight() { return scaleWithDpi(16); } 13580 override int minHeight() { return scaleWithDpi(16); } 13581 } else version(custom_widgets) { 13582 private enum buttonSize = 16; 13583 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 13584 override int minHeight() { return maxHeight(); } 13585 } else static assert(0); 13586 13587 override int marginLeft() { return 4; } 13588 13589 // FIXME: make a label getter 13590 private string label; 13591 private dchar accelerator; 13592 13593 /++ 13594 13595 +/ 13596 this(string label, Widget parent) { 13597 super(parent); 13598 version(win32_widgets) { 13599 this.label = label; 13600 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 13601 } else version(custom_widgets) { 13602 label.extractWindowsStyleLabel(this.label, this.accelerator); 13603 height = 16; 13604 width = height + 4 + cast(int) label.length * 16; 13605 } 13606 } 13607 13608 version(custom_widgets) 13609 override void paint(WidgetPainter painter) { 13610 auto cs = getComputedStyle(); 13611 13612 if(isFocused) { 13613 painter.fillColor = cs.windowBackgroundColor; 13614 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 13615 } else { 13616 painter.fillColor = cs.windowBackgroundColor; 13617 painter.outlineColor = cs.windowBackgroundColor; 13618 } 13619 painter.drawRectangle(Point(0, 0), width, height); 13620 13621 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 13622 13623 painter.outlineColor = Color.black; 13624 painter.fillColor = Color.white; 13625 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 13626 if(isChecked) { 13627 painter.outlineColor = Color.black; 13628 painter.fillColor = Color.black; 13629 // I'm using height so the checkbox is square 13630 auto size = scaleWithDpi(2); 13631 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 13632 } 13633 13634 painter.outlineColor = cs.foregroundColor(); 13635 painter.fillColor = cs.foregroundColor(); 13636 13637 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 13638 } 13639 13640 13641 override void defaultEventHandler_triggered(Event ev) { 13642 isChecked = true; 13643 13644 if(this.parent) { 13645 foreach(child; this.parent.children) { 13646 if(child is this) continue; 13647 if(auto rb = cast(Radiobox) child) { 13648 rb.isChecked = false; 13649 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 13650 rb.redraw(); 13651 } 13652 } 13653 } 13654 13655 this.emit!(ChangeEvent!bool)(&this.isChecked); 13656 13657 redraw(); 13658 } 13659 13660 /// 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. 13661 mixin Emits!(ChangeEvent!bool); 13662 } 13663 13664 13665 /++ 13666 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 13667 13668 13669 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 13670 13671 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13672 13673 History: 13674 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. 13675 +/ 13676 class Button : MouseActivatedWidget { 13677 override int heightStretchiness() { return 3; } 13678 override int widthStretchiness() { return 3; } 13679 13680 /++ 13681 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. 13682 13683 History: 13684 Added July 2, 2021 13685 +/ 13686 public bool triggersOnMultiClick; 13687 13688 private string label_; 13689 private TextAlignment alignment; 13690 private dchar accelerator; 13691 13692 /// 13693 string label() { return label_; } 13694 /// 13695 void label(string l) { 13696 label_ = l; 13697 version(win32_widgets) { 13698 WCharzBuffer bfr = WCharzBuffer(l); 13699 SetWindowTextW(hwnd, bfr.ptr); 13700 } else version(custom_widgets) { 13701 redraw(); 13702 } 13703 } 13704 13705 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 13706 super.defaultEventHandler_dblclick(ev); 13707 if(triggersOnMultiClick) { 13708 if(ev.button == MouseButton.left) { 13709 auto event = new Event(EventType.triggered, this); 13710 event.sendDirectly(); 13711 } 13712 } 13713 } 13714 13715 private Sprite sprite; 13716 private int displayFlags; 13717 13718 protected bool needsOwnerDraw() { 13719 return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent; 13720 } 13721 13722 version(win32_widgets) 13723 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) { 13724 auto itemId = dis.itemID; 13725 auto hdc = dis.hDC; 13726 auto rect = dis.rcItem; 13727 switch(dis.itemAction) { 13728 // skipping setDynamicState because i don't want to queue the redraw unnecessarily 13729 case ODA_SELECT: 13730 dynamicState_ &= ~DynamicState.depressed; 13731 if(dis.itemState & ODS_SELECTED) 13732 dynamicState_ |= DynamicState.depressed; 13733 goto case; 13734 case ODA_FOCUS: 13735 dynamicState_ &= ~DynamicState.focus; 13736 if(dis.itemState & ODS_FOCUS) 13737 dynamicState_ |= DynamicState.focus; 13738 goto case; 13739 case ODA_DRAWENTIRE: 13740 auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this); 13741 //painter.impl.hdc = hdc; 13742 paint(painter); 13743 break; 13744 default: 13745 } 13746 return 1; 13747 13748 } 13749 13750 /++ 13751 Creates a push button with the given label, which may be an image or some text. 13752 13753 Bugs: 13754 If the image is bigger than the button, it may not be displayed in the right position on Linux. 13755 13756 History: 13757 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 13758 13759 The button with label and image will respect requests to show both on Windows as 13760 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 13761 +/ 13762 this(string label, Widget parent) { 13763 this(ImageLabel(label), parent); 13764 } 13765 13766 /// ditto 13767 this(ImageLabel label, Widget parent) { 13768 bool needsImage; 13769 version(win32_widgets) { 13770 super(parent); 13771 13772 // BS_BITMAP is set when we want image only, so checking for exactly that combination 13773 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 13774 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 13775 13776 // could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check. 13777 13778 if(needsOwnerDraw) { 13779 extraStyle |= BS_OWNERDRAW; 13780 needsImage = true; 13781 } 13782 13783 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 13784 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 13785 13786 if(label.image) { 13787 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 13788 13789 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 13790 } 13791 13792 this.label = label.label; 13793 } else version(custom_widgets) { 13794 super(parent); 13795 13796 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 13797 needsImage = true; 13798 } 13799 13800 13801 if(needsImage && label.image) { 13802 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 13803 this.displayFlags = label.displayFlags; 13804 } 13805 13806 this.alignment = label.alignment; 13807 } 13808 13809 override int minHeight() { return defaultLineHeight + 4; } 13810 13811 static class Style : Widget.Style { 13812 override WidgetBackground background() { 13813 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 13814 13815 auto pressed = DynamicState.depressed | DynamicState.hover; 13816 if((widget.dynamicState & pressed) == pressed && widget.enabled) { 13817 return WidgetBackground(cs.depressedButtonColor()); 13818 } else if(widget.dynamicState & DynamicState.hover && widget.enabled) { 13819 return WidgetBackground(cs.hoveringColor()); 13820 } else { 13821 return WidgetBackground(cs.buttonColor()); 13822 } 13823 } 13824 13825 override Color foregroundColor() { 13826 auto clr = super.foregroundColor(); 13827 if(widget.enabled) return clr; 13828 13829 return Color(clr.r, clr.g, clr.b, clr.a / 2); 13830 } 13831 13832 override FrameStyle borderStyle() { 13833 auto pressed = DynamicState.depressed | DynamicState.hover; 13834 if((widget.dynamicState & pressed) == pressed && widget.enabled) { 13835 return FrameStyle.sunk; 13836 } else { 13837 return FrameStyle.risen; 13838 } 13839 13840 } 13841 13842 override bool variesWithState(ulong dynamicStateFlags) { 13843 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 13844 } 13845 } 13846 mixin OverrideStyle!Style; 13847 13848 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13849 if(sprite) { 13850 sprite.drawAt( 13851 painter, 13852 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 13853 Point(0, 0) 13854 ); 13855 } else { 13856 Point pos = bounds.upperLeft; 13857 if(this.height == 16) 13858 pos.y -= 2; // total hack omg 13859 13860 painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 13861 } 13862 return bounds; 13863 } 13864 13865 override int flexBasisWidth() { 13866 version(win32_widgets) { 13867 SIZE size; 13868 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13869 if(size.cx == 0) 13870 goto fallback; 13871 return size.cx + scaleWithDpi(16); 13872 } 13873 fallback: 13874 return scaleWithDpi(cast(int) label.length * 8 + 16); 13875 } 13876 13877 override int flexBasisHeight() { 13878 version(win32_widgets) { 13879 SIZE size; 13880 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13881 if(size.cy == 0) 13882 goto fallback; 13883 return size.cy + scaleWithDpi(6); 13884 } 13885 fallback: 13886 return defaultLineHeight + 4; 13887 } 13888 } 13889 13890 /++ 13891 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. 13892 13893 History: 13894 Added January 14, 2024 13895 +/ 13896 class CustomButton : Button { 13897 this(ImageLabel label, Widget parent) { 13898 super(label, parent); 13899 } 13900 13901 this(string label, Widget parent) { 13902 super(label, parent); 13903 } 13904 13905 version(win32_widgets) 13906 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 13907 // paint is driven by handleWmDrawItem instead of minigui's redraw events 13908 if(hwnd) 13909 InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw 13910 return; 13911 } 13912 13913 override void paint(WidgetPainter painter) { 13914 // the parent does `if(hwnd) return;` because 13915 // normally we don't want to draw on standard controls, 13916 // but this is an exception if it is an owner drawn button 13917 // (which is determined in the constructor by testing, 13918 // at runtime, for the existence of an overridden paint 13919 // member anyway, so this needed to trigger BS_OWNERDRAW) 13920 // sdpyPrintDebugString("drawing"); 13921 painter.drawThemed(&paintContent); 13922 } 13923 } 13924 13925 /++ 13926 A button with a consistent size, suitable for user commands like OK and CANCEL. 13927 +/ 13928 class CommandButton : Button { 13929 this(string label, Widget parent) { 13930 super(label, parent); 13931 } 13932 13933 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 13934 13935 override int maxHeight() { 13936 return defaultLineHeight + 4; 13937 } 13938 13939 override int maxWidth() { 13940 return defaultLineHeight * 4; 13941 } 13942 13943 override int marginLeft() { return 12; } 13944 override int marginRight() { return 12; } 13945 override int marginTop() { return 12; } 13946 override int marginBottom() { return 12; } 13947 } 13948 13949 /// 13950 enum ArrowDirection { 13951 left, /// 13952 right, /// 13953 up, /// 13954 down /// 13955 } 13956 13957 /// 13958 version(custom_widgets) 13959 class ArrowButton : Button { 13960 /// 13961 this(ArrowDirection direction, Widget parent) { 13962 super("", parent); 13963 this.direction = direction; 13964 triggersOnMultiClick = true; 13965 } 13966 13967 private ArrowDirection direction; 13968 13969 override int minHeight() { return scaleWithDpi(16); } 13970 override int maxHeight() { return scaleWithDpi(16); } 13971 override int minWidth() { return scaleWithDpi(16); } 13972 override int maxWidth() { return scaleWithDpi(16); } 13973 13974 override void paint(WidgetPainter painter) { 13975 super.paint(painter); 13976 13977 auto cs = getComputedStyle(); 13978 13979 painter.outlineColor = cs.foregroundColor; 13980 painter.fillColor = cs.foregroundColor; 13981 13982 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 13983 13984 final switch(direction) { 13985 case ArrowDirection.up: 13986 painter.drawPolygon( 13987 scaleWithDpi(Point(2, 10) + offset), 13988 scaleWithDpi(Point(7, 5) + offset), 13989 scaleWithDpi(Point(12, 10) + offset), 13990 scaleWithDpi(Point(2, 10) + offset) 13991 ); 13992 break; 13993 case ArrowDirection.down: 13994 painter.drawPolygon( 13995 scaleWithDpi(Point(2, 6) + offset), 13996 scaleWithDpi(Point(7, 11) + offset), 13997 scaleWithDpi(Point(12, 6) + offset), 13998 scaleWithDpi(Point(2, 6) + offset) 13999 ); 14000 break; 14001 case ArrowDirection.left: 14002 painter.drawPolygon( 14003 scaleWithDpi(Point(10, 2) + offset), 14004 scaleWithDpi(Point(5, 7) + offset), 14005 scaleWithDpi(Point(10, 12) + offset), 14006 scaleWithDpi(Point(10, 2) + offset) 14007 ); 14008 break; 14009 case ArrowDirection.right: 14010 painter.drawPolygon( 14011 scaleWithDpi(Point(6, 2) + offset), 14012 scaleWithDpi(Point(11, 7) + offset), 14013 scaleWithDpi(Point(6, 12) + offset), 14014 scaleWithDpi(Point(6, 2) + offset) 14015 ); 14016 break; 14017 } 14018 } 14019 } 14020 14021 private 14022 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 14023 int x, y; 14024 Widget par = c; 14025 while(par) { 14026 x += par.x; 14027 y += par.y; 14028 par = par.parent; 14029 } 14030 return [x, y]; 14031 } 14032 14033 version(win32_widgets) 14034 private 14035 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 14036 // MapWindowPoints? 14037 int x, y; 14038 Widget par = c; 14039 while(par) { 14040 x += par.x; 14041 y += par.y; 14042 par = par.parent; 14043 if(par !is null && par.useNativeDrawing()) 14044 break; 14045 } 14046 return [x, y]; 14047 } 14048 14049 /// 14050 class ImageBox : Widget { 14051 private MemoryImage image_; 14052 14053 override int widthStretchiness() { return 1; } 14054 override int heightStretchiness() { return 1; } 14055 override int widthShrinkiness() { return 1; } 14056 override int heightShrinkiness() { return 1; } 14057 14058 override int flexBasisHeight() { 14059 return image_.height; 14060 } 14061 14062 override int flexBasisWidth() { 14063 return image_.width; 14064 } 14065 14066 /// 14067 public void setImage(MemoryImage image){ 14068 this.image_ = image; 14069 if(this.parentWindow && this.parentWindow.win) { 14070 if(sprite) 14071 sprite.dispose(); 14072 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 14073 } 14074 redraw(); 14075 } 14076 14077 /// How to fit the image in the box if they aren't an exact match in size? 14078 enum HowToFit { 14079 center, /// centers the image, cropping around all the edges as needed 14080 crop, /// always draws the image in the upper left, cropping the lower right if needed 14081 // stretch, /// not implemented 14082 } 14083 14084 private Sprite sprite; 14085 private HowToFit howToFit_; 14086 14087 private Color backgroundColor_; 14088 14089 /// 14090 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 14091 this.image_ = image; 14092 this.tabStop = false; 14093 this.howToFit_ = howToFit; 14094 this.backgroundColor_ = backgroundColor; 14095 super(parent); 14096 updateSprite(); 14097 } 14098 14099 /// ditto 14100 this(MemoryImage image, HowToFit howToFit, Widget parent) { 14101 this(image, howToFit, Color.transparent, parent); 14102 } 14103 14104 private void updateSprite() { 14105 if(sprite is null && this.parentWindow && this.parentWindow.win) { 14106 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 14107 } 14108 } 14109 14110 override void paint(WidgetPainter painter) { 14111 updateSprite(); 14112 if(backgroundColor_.a) { 14113 painter.fillColor = backgroundColor_; 14114 painter.drawRectangle(Point(0, 0), width, height); 14115 } 14116 if(howToFit_ == HowToFit.crop) 14117 sprite.drawAt(painter, Point(0, 0)); 14118 else if(howToFit_ == HowToFit.center) { 14119 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 14120 } 14121 } 14122 } 14123 14124 /// 14125 class TextLabel : Widget { 14126 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 14127 override int maxHeight() { return minHeight; } 14128 override int minWidth() { return 32; } 14129 14130 override int flexBasisHeight() { return minHeight(); } 14131 override int flexBasisWidth() { return defaultTextWidth(label); } 14132 14133 string label_; 14134 14135 /++ 14136 Indicates which other control this label is here for. Similar to HTML `for` attribute. 14137 14138 In practice this means a click on the label will focus the `labelFor`. In future versions 14139 it will also set screen reader hints but that is not yet implemented. 14140 14141 History: 14142 Added October 3, 2021 (dub v10.4) 14143 +/ 14144 Widget labelFor; 14145 14146 /// 14147 @scriptable 14148 string label() { return label_; } 14149 14150 /// 14151 @scriptable 14152 void label(string l) { 14153 label_ = l; 14154 version(win32_widgets) { 14155 WCharzBuffer bfr = WCharzBuffer(l); 14156 SetWindowTextW(hwnd, bfr.ptr); 14157 } else version(custom_widgets) 14158 redraw(); 14159 } 14160 14161 override void defaultEventHandler_click(scope ClickEvent ce) { 14162 if(this.labelFor !is null) 14163 this.labelFor.focus(); 14164 } 14165 14166 /++ 14167 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 14168 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 14169 +/ 14170 this(string label, TextAlignment alignment, Widget parent) { 14171 this.label_ = label; 14172 this.alignment = alignment; 14173 this.tabStop = false; 14174 super(parent); 14175 14176 version(win32_widgets) 14177 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 14178 } 14179 14180 /// ditto 14181 this(string label, Widget parent) { 14182 this(label, TextAlignment.Right, parent); 14183 } 14184 14185 TextAlignment alignment; 14186 14187 version(custom_widgets) 14188 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 14189 painter.outlineColor = getComputedStyle().foregroundColor; 14190 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 14191 return bounds; 14192 } 14193 } 14194 14195 class TextDisplayHelper : Widget { 14196 protected TextLayouter l; 14197 protected ScrollMessageWidget smw; 14198 14199 private const(TextLayouter.State)*[] undoStack; 14200 private const(TextLayouter.State)*[] redoStack; 14201 14202 private string preservedPrimaryText; 14203 protected void selectionChanged() { 14204 // sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString()); 14205 static if(UsingSimpledisplayX11) 14206 with(l.selection()) { 14207 if(!isEmpty()) { 14208 //sdpyPrintDebugString("!isEmpty"); 14209 14210 getPrimarySelection(parentWindow.win, (in char[] txt) { 14211 // sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")"); 14212 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 14213 if(txt.length) { 14214 preservedPrimaryText = txt.idup; 14215 // writeln(preservedPrimaryText); 14216 } 14217 14218 setPrimarySelection(parentWindow.win, getContentString()); 14219 }); 14220 } 14221 } 14222 } 14223 14224 final TextLayouter layouter() { 14225 return l; 14226 } 14227 14228 bool readonly; 14229 bool caretNavigation; // scroll lock can flip this 14230 bool singleLine; 14231 bool acceptsTabInput; 14232 14233 private Menu ctx; 14234 override Menu contextMenu(int x, int y) { 14235 if(ctx is null) { 14236 ctx = new Menu("Actions", this); 14237 if(!readonly) { 14238 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 14239 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 14240 ctx.addSeparator(); 14241 } 14242 if(!readonly) 14243 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 14244 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 14245 if(!readonly) 14246 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 14247 if(!readonly) 14248 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 14249 ctx.addSeparator(); 14250 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 14251 } 14252 return ctx; 14253 } 14254 14255 override void defaultEventHandler_blur(BlurEvent ev) { 14256 super.defaultEventHandler_blur(ev); 14257 if(l.wasMutated()) { 14258 auto evt = new ChangeEvent!string(this, &this.content); 14259 evt.dispatch(); 14260 l.clearWasMutatedFlag(); 14261 } 14262 } 14263 14264 private string content() { 14265 return l.getTextString(); 14266 } 14267 14268 void undo() { 14269 if(readonly) return; 14270 if(undoStack.length) { 14271 auto state = undoStack[$-1]; 14272 undoStack = undoStack[0 .. $-1]; 14273 undoStack.assumeSafeAppend(); 14274 redoStack ~= l.saveState(); 14275 l.restoreState(state); 14276 adjustScrollbarSizes(); 14277 scrollForCaret(); 14278 redraw(); 14279 stateCheckpoint = true; 14280 } 14281 } 14282 14283 void redo() { 14284 if(readonly) return; 14285 if(redoStack.length) { 14286 doStateCheckpoint(); 14287 auto state = redoStack[$-1]; 14288 redoStack = redoStack[0 .. $-1]; 14289 redoStack.assumeSafeAppend(); 14290 l.restoreState(state); 14291 adjustScrollbarSizes(); 14292 scrollForCaret(); 14293 redraw(); 14294 stateCheckpoint = true; 14295 } 14296 } 14297 14298 void cut() { 14299 if(readonly) return; 14300 with(l.selection()) { 14301 if(!isEmpty()) { 14302 setClipboardText(parentWindow.win, getContentString()); 14303 doStateCheckpoint(); 14304 replaceContent(""); 14305 adjustScrollbarSizes(); 14306 scrollForCaret(); 14307 this.redraw(); 14308 } 14309 } 14310 14311 } 14312 14313 void copy() { 14314 with(l.selection()) { 14315 if(!isEmpty()) { 14316 setClipboardText(parentWindow.win, getContentString()); 14317 this.redraw(); 14318 } 14319 } 14320 } 14321 14322 void paste() { 14323 if(readonly) return; 14324 getClipboardText(parentWindow.win, (txt) { 14325 doStateCheckpoint(); 14326 if(singleLine) 14327 l.selection.replaceContent(txt.stripInternal()); 14328 else 14329 l.selection.replaceContent(txt); 14330 adjustScrollbarSizes(); 14331 scrollForCaret(); 14332 this.redraw(); 14333 }); 14334 } 14335 14336 void deleteContentOfSelection() { 14337 if(readonly) return; 14338 doStateCheckpoint(); 14339 l.selection.replaceContent(""); 14340 l.selection.setUserXCoordinate(); 14341 adjustScrollbarSizes(); 14342 scrollForCaret(); 14343 redraw(); 14344 } 14345 14346 void selectAll() { 14347 with(l.selection) { 14348 moveToStartOfDocument(); 14349 setAnchor(); 14350 moveToEndOfDocument(); 14351 setFocus(); 14352 14353 selectionChanged(); 14354 } 14355 redraw(); 14356 } 14357 14358 protected bool stateCheckpoint = true; 14359 14360 protected void doStateCheckpoint() { 14361 if(stateCheckpoint) { 14362 undoStack ~= l.saveState(); 14363 stateCheckpoint = false; 14364 } 14365 } 14366 14367 protected void adjustScrollbarSizes() { 14368 // FIXME: will want a content area helper function instead of doing all these subtractions myself 14369 auto borderWidth = 2; 14370 this.smw.setTotalArea(l.width, l.height); 14371 this.smw.setViewableArea( 14372 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 14373 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 14374 } 14375 14376 protected void scrollForCaret() { 14377 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 14378 smw.scrollIntoView(l.selection.focusBoundingBox()); 14379 } 14380 14381 // FIXME: this should be a theme changed event listener instead 14382 private BaseVisualTheme currentTheme; 14383 override void recomputeChildLayout() { 14384 if(currentTheme is null) 14385 currentTheme = WidgetPainter.visualTheme; 14386 if(WidgetPainter.visualTheme !is currentTheme) { 14387 currentTheme = WidgetPainter.visualTheme; 14388 auto ds = this.l.defaultStyle; 14389 if(auto ms = cast(MyTextStyle) ds) { 14390 auto cs = getComputedStyle(); 14391 auto font = cs.font(); 14392 if(font !is null) 14393 ms.font_ = font; 14394 else { 14395 auto osc = new OperatingSystemFont(); 14396 osc.loadDefault; 14397 ms.font_ = osc; 14398 } 14399 } 14400 } 14401 super.recomputeChildLayout(); 14402 } 14403 14404 private Point adjustForSingleLine(Point p) { 14405 if(singleLine) 14406 return Point(p.x, this.height / 2); 14407 else 14408 return p; 14409 } 14410 14411 private bool wordWrapEnabled_; 14412 14413 this(TextLayouter l, ScrollMessageWidget parent) { 14414 this.smw = parent; 14415 14416 smw.addDefaultWheelListeners(16, 16, 8); 14417 smw.movementPerButtonClick(16, 16); 14418 14419 this.defaultPadding = Rectangle(2, 2, 2, 2); 14420 14421 this.l = l; 14422 super(parent); 14423 14424 smw.addEventListener((scope ScrollEvent se) { 14425 this.redraw(); 14426 }); 14427 14428 this.addEventListener((scope ResizeEvent re) { 14429 // FIXME: I should add a method to give this client area width thing 14430 if(wordWrapEnabled_) 14431 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 14432 14433 adjustScrollbarSizes(); 14434 scrollForCaret(); 14435 14436 this.redraw(); 14437 }); 14438 14439 } 14440 14441 private { 14442 bool mouseDown; 14443 bool mouseActuallyMoved; 14444 14445 Point downAt; 14446 14447 Timer autoscrollTimer; 14448 int autoscrollDirection; 14449 int autoscrollAmount; 14450 14451 void autoscroll() { 14452 switch(autoscrollDirection) { 14453 case 0: smw.scrollUp(autoscrollAmount); break; 14454 case 1: smw.scrollDown(autoscrollAmount); break; 14455 case 2: smw.scrollLeft(autoscrollAmount); break; 14456 case 3: smw.scrollRight(autoscrollAmount); break; 14457 default: assert(0); 14458 } 14459 14460 this.redraw(); 14461 } 14462 14463 void setAutoscrollTimer(int direction, int amount) { 14464 if(autoscrollTimer is null) { 14465 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 14466 } 14467 14468 autoscrollDirection = direction; 14469 autoscrollAmount = amount; 14470 } 14471 14472 void stopAutoscrollTimer() { 14473 if(autoscrollTimer !is null) { 14474 autoscrollTimer.dispose(); 14475 autoscrollTimer = null; 14476 } 14477 autoscrollAmount = 0; 14478 autoscrollDirection = 0; 14479 } 14480 } 14481 14482 override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) { 14483 if(mouseDown) { 14484 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 14485 14486 // FIXME: when scrolling i actually do want a timer. 14487 // i also want a zone near the sides of the window where i can auto scroll 14488 14489 auto scrollMultiplier = scaleWithDpi(16); 14490 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 14491 14492 if(!singleLine && movedTo.y < 4) { 14493 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 14494 } else 14495 if(!singleLine && (movedTo.y + 6) > this.height) { 14496 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 14497 } else 14498 if(movedTo.x < 4) { 14499 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 14500 } else 14501 if((movedTo.x + 6) > this.width) { 14502 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 14503 } else 14504 stopAutoscrollTimer(); 14505 14506 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 14507 l.selection.setFocus(); 14508 mouseActuallyMoved = true; 14509 this.redraw(); 14510 } 14511 14512 super.defaultEventHandler_mousemove(ce); 14513 } 14514 14515 override void defaultEventHandler_mouseup(scope MouseUpEvent ce) { 14516 // FIXME: assert primary selection 14517 if(mouseDown && ce.button == MouseButton.left) { 14518 stateCheckpoint = true; 14519 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 14520 //l.selection.setFocus(); 14521 mouseDown = false; 14522 parentWindow.releaseMouseCapture(); 14523 stopAutoscrollTimer(); 14524 this.redraw(); 14525 14526 if(mouseActuallyMoved) 14527 selectionChanged(); 14528 } 14529 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 14530 14531 super.defaultEventHandler_mouseup(ce); 14532 } 14533 14534 static if(UsingSimpledisplayX11) 14535 override void defaultEventHandler_click(scope ClickEvent ce) { 14536 if(ce.button == MouseButton.middle) { 14537 parentWindow.win.getPrimarySelection((txt) { 14538 doStateCheckpoint(); 14539 14540 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 14541 14542 if(txt == l.selection.getContentString && preservedPrimaryText.length) 14543 l.selection.replaceContent(preservedPrimaryText); 14544 else 14545 l.selection.replaceContent(txt); 14546 redraw(); 14547 }); 14548 } 14549 14550 super.defaultEventHandler_click(ce); 14551 } 14552 14553 final const(char)[] wordSplitHelper(scope return const(char)[] ch) { 14554 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 14555 return ch; 14556 return null; 14557 } 14558 14559 override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) { 14560 if(dce.button == MouseButton.left) { 14561 with(l.selection()) { 14562 // FIXME: for a url or file picker i might wanna use / as a separator intead 14563 scope dg = &wordSplitHelper; 14564 find(dg, 1, true).moveToEnd.setAnchor; 14565 find(dg, 1, false).moveTo.setFocus; 14566 selectionChanged(); 14567 redraw(); 14568 } 14569 } 14570 14571 super.defaultEventHandler_dblclick(dce); 14572 } 14573 14574 override void defaultEventHandler_mousedown(scope MouseDownEvent ce) { 14575 if(ce.button == MouseButton.left) { 14576 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 14577 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 14578 if(ce.shiftKey) 14579 l.selection.setFocus(); 14580 else 14581 l.selection.setAnchor(); 14582 mouseDown = true; 14583 mouseActuallyMoved = false; 14584 parentWindow.captureMouse(this); 14585 this.redraw(); 14586 } 14587 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 14588 14589 super.defaultEventHandler_mousedown(ce); 14590 } 14591 14592 override void defaultEventHandler_char(scope CharEvent ce) { 14593 super.defaultEventHandler_char(ce); 14594 14595 if(readonly) 14596 return; 14597 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 14598 return; // skip the ctrl+x characters we don't care about as plain text 14599 14600 if(singleLine && ce.character == '\n') 14601 return; 14602 if(!acceptsTabInput && ce.character == '\t') 14603 return; 14604 14605 doStateCheckpoint(); 14606 14607 char[4] buffer; 14608 import arsd.core; 14609 auto stride = encodeUtf8(buffer, ce.character); 14610 l.selection.replaceContent(buffer[0 .. stride]); 14611 l.selection.setUserXCoordinate(); 14612 adjustScrollbarSizes(); 14613 scrollForCaret(); 14614 redraw(); 14615 14616 } 14617 14618 override void defaultEventHandler_keydown(scope KeyDownEvent kde) { 14619 switch(kde.key) { 14620 case Key.Up, Key.Down, Key.Left, Key.Right: 14621 case Key.Home, Key.End: 14622 stateCheckpoint = true; 14623 bool setPosition = false; 14624 switch(kde.key) { 14625 case Key.Up: l.selection.moveUp(); break; 14626 case Key.Down: l.selection.moveDown(); break; 14627 case Key.Left: 14628 l.selection.moveLeft(); 14629 14630 if(kde.ctrlKey) { 14631 l.selection.find(&wordSplitHelper, 1, true).moveToEnd; 14632 } 14633 14634 setPosition = true; 14635 break; 14636 case Key.Right: 14637 l.selection.moveRight(); 14638 14639 if(kde.ctrlKey) { 14640 l.selection.find(&wordSplitHelper, 1, false).moveTo; 14641 } 14642 14643 setPosition = true; 14644 break; 14645 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 14646 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 14647 default: assert(0); 14648 } 14649 14650 if(kde.shiftKey) 14651 l.selection.setFocus(); 14652 else 14653 l.selection.setAnchor(); 14654 14655 selectionChanged(); 14656 14657 if(setPosition) 14658 l.selection.setUserXCoordinate(); 14659 scrollForCaret(); 14660 redraw(); 14661 break; 14662 case Key.PageUp, Key.PageDown: 14663 // want to act like the user clicked on the caret again 14664 // after the scroll operation completed, so it would remain at 14665 // about the same place on the viewport 14666 auto oldY = smw.vsb.position; 14667 smw.defaultKeyboardListener(kde); 14668 auto newY = smw.vsb.position; 14669 with(l.selection) { 14670 auto uc = getUserCoordinate(); 14671 uc.y += newY - oldY; 14672 moveTo(uc); 14673 14674 if(kde.shiftKey) 14675 setFocus(); 14676 else 14677 setAnchor(); 14678 } 14679 break; 14680 case Key.Delete: 14681 if(l.selection.isEmpty()) { 14682 l.selection.setAnchor(); 14683 l.selection.moveRight(); 14684 l.selection.setFocus(); 14685 } 14686 deleteContentOfSelection(); 14687 adjustScrollbarSizes(); 14688 scrollForCaret(); 14689 break; 14690 case Key.Insert: 14691 break; 14692 case Key.A: 14693 if(kde.ctrlKey) 14694 selectAll(); 14695 break; 14696 case Key.F: 14697 // find 14698 break; 14699 case Key.Z: 14700 if(kde.ctrlKey) 14701 undo(); 14702 break; 14703 case Key.R: 14704 if(kde.ctrlKey) 14705 redo(); 14706 break; 14707 case Key.X: 14708 if(kde.ctrlKey) 14709 cut(); 14710 break; 14711 case Key.C: 14712 if(kde.ctrlKey) 14713 copy(); 14714 break; 14715 case Key.V: 14716 if(kde.ctrlKey) 14717 paste(); 14718 break; 14719 case Key.F1: 14720 with(l.selection()) { 14721 moveToStartOfLine(); 14722 setAnchor(); 14723 moveToEndOfLine(); 14724 moveToIncludeAdjacentEndOfLineMarker(); 14725 setFocus(); 14726 replaceContent(""); 14727 } 14728 14729 redraw(); 14730 break; 14731 /* 14732 case Key.F2: 14733 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 14734 //(cast(MyTextStyle) old).font, 14735 font2, 14736 Color.red))); 14737 redraw(); 14738 break; 14739 */ 14740 case Key.Tab: 14741 // we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl 14742 if(acceptsTabInput && !kde.ctrlKey) 14743 kde.preventDefault(); 14744 break; 14745 default: 14746 } 14747 14748 if(!kde.defaultPrevented) 14749 super.defaultEventHandler_keydown(kde); 14750 } 14751 14752 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 14753 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 14754 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent 14755 if(parent && parent.parent && parent.parent.parent) 14756 parent.parent.parent.useStyleProperties(dg); 14757 else 14758 super.useStyleProperties(dg); 14759 } 14760 14761 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 14762 override int maxHeight() { 14763 if(singleLine) 14764 return minHeight; 14765 else 14766 return super.maxHeight(); 14767 } 14768 14769 void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14770 painter.setFont(myStyle.font); 14771 painter.drawText(upperLeft, text); 14772 } 14773 14774 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 14775 //painter.setFont(font); 14776 14777 auto cs = getComputedStyle(); 14778 auto defaultColor = cs.foregroundColor; 14779 14780 auto old = painter.setClipRectangleForWidget(bounds.upperLeft, bounds.width, bounds.height); 14781 scope(exit) painter.setClipRectangleForWidget(old.upperLeft, old.width, old.height); 14782 14783 l.getDrawableText(delegate bool(txt, style, info, carets...) { 14784 //writeln("Segment: ", txt); 14785 assert(style !is null); 14786 14787 if(info.selections && info.boundingBox.width > 0) { 14788 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 14789 painter.fillColor = color; 14790 painter.outlineColor = color; 14791 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 14792 painter.outlineColor = cs.selectionForegroundColor; 14793 //painter.fillColor = Color.white; 14794 } else { 14795 painter.outlineColor = defaultColor; 14796 } 14797 14798 if(this.isFocused) 14799 foreach(idx, caret; carets) { 14800 if(idx == 0) 14801 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 14802 painter.drawLine( 14803 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 14804 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 14805 ); 14806 } 14807 14808 if(txt.stripInternal.length) { 14809 // defaultColor = myStyle.color; // FIXME: so wrong 14810 if(auto myStyle = cast(MyTextStyle) style) 14811 drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 14812 else if(auto myStyle = cast(MyImageStyle) style) 14813 myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 14814 } 14815 14816 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 14817 return false; 14818 } else { 14819 return true; 14820 } 14821 }, Rectangle(smw.position(), bounds.size)); 14822 14823 /+ 14824 int place = 0; 14825 int y = 75; 14826 foreach(width; widths) { 14827 painter.fillColor = Color.red; 14828 painter.drawRectangle(Point(place, y), Size(width, 75)); 14829 //y += 15; 14830 place += width; 14831 } 14832 +/ 14833 14834 return bounds; 14835 } 14836 14837 static class MyTextStyle : TextStyle { 14838 OperatingSystemFont font_; 14839 this(OperatingSystemFont font, bool passwordMode = false) { 14840 this.font_ = font; 14841 } 14842 14843 override OperatingSystemFont font() { 14844 return font_; 14845 } 14846 14847 bool foregroundColorOverridden; 14848 bool backgroundColorOverridden; 14849 Color foregroundColor; 14850 Color backgroundColor; // should this be inline segment or the whole paragraph block? 14851 bool italic; 14852 bool bold; 14853 bool underline; 14854 bool strikeout; 14855 bool subscript; 14856 bool superscript; 14857 } 14858 14859 static class MyImageStyle : TextStyle, MeasurableFont { 14860 MemoryImage image_; 14861 Image converted; 14862 this(MemoryImage image) { 14863 this.image_ = image; 14864 this.converted = Image.fromMemoryImage(image); 14865 } 14866 14867 bool isMonospace() { return false; } 14868 fnum averageWidth() { return image_.width; } 14869 fnum height() { return image_.height; } 14870 fnum ascent() { return image_.height; } 14871 fnum descent() { return 0; } 14872 14873 fnum stringWidth(scope const(char)[] s, SimpleWindow window = null) { 14874 return image_.width; 14875 } 14876 14877 override MeasurableFont font() { 14878 return this; 14879 } 14880 14881 void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14882 painter.drawImage(upperLeft, converted); 14883 } 14884 } 14885 } 14886 14887 /+ 14888 class TextWidget : Widget { 14889 TextLayouter l; 14890 ScrollMessageWidget smw; 14891 TextDisplayHelper helper; 14892 this(TextLayouter l, Widget parent) { 14893 this.l = l; 14894 super(parent); 14895 14896 smw = new ScrollMessageWidget(this); 14897 //smw.horizontalScrollBar.hide; 14898 //smw.verticalScrollBar.hide; 14899 smw.addDefaultWheelListeners(16, 16, 8); 14900 smw.movementPerButtonClick(16, 16); 14901 helper = new TextDisplayHelper(l, smw); 14902 14903 // no need to do this here since there's gonna be a resize 14904 // event immediately before any drawing 14905 // smw.setTotalArea(l.width, l.height); 14906 smw.setViewableArea( 14907 this.width - this.paddingLeft - this.paddingRight, 14908 this.height - this.paddingTop - this.paddingBottom); 14909 14910 /+ 14911 writeln(l.width, "x", l.height); 14912 +/ 14913 } 14914 } 14915 +/ 14916 14917 14918 14919 14920 /+ 14921 make sure it calls parentWindow.inputProxy.setIMEPopupLocation too 14922 +/ 14923 14924 /++ 14925 Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit]. 14926 +/ 14927 abstract class EditableTextWidget : Widget { 14928 protected this(Widget parent) { 14929 version(custom_widgets) 14930 this(true, parent); 14931 else 14932 this(false, parent); 14933 } 14934 14935 private bool useCustomWidget; 14936 14937 protected this(bool useCustomWidget, Widget parent) { 14938 this.useCustomWidget = useCustomWidget; 14939 14940 super(parent); 14941 14942 if(useCustomWidget) 14943 setupCustomTextEditing(); 14944 } 14945 14946 private bool wordWrapEnabled_; 14947 /++ 14948 Enables or disables wrapping of long lines on word boundaries. 14949 +/ 14950 void wordWrapEnabled(bool enabled) { 14951 if(useCustomWidget) { 14952 wordWrapEnabled_ = enabled; 14953 if(tdh) 14954 tdh.wordWrapEnabled_ = true; 14955 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 14956 } else version(win32_widgets) { 14957 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 14958 } 14959 } 14960 14961 override int minWidth() { return scaleWithDpi(16); } 14962 override int widthStretchiness() { return 7; } 14963 override int widthShrinkiness() { return 1; } 14964 14965 override int maxHeight() { 14966 if(useCustomWidget) 14967 return tdh.maxHeight; 14968 else 14969 return super.maxHeight(); 14970 } 14971 14972 override void focus() { 14973 if(useCustomWidget && tdh) 14974 tdh.focus(); 14975 else 14976 super.focus(); 14977 } 14978 14979 override void defaultEventHandler_focusout(FocusOutEvent foe) { 14980 if(tdh !is null && foe.target is tdh) 14981 tdh.redraw(); 14982 } 14983 14984 override void defaultEventHandler_focusin(FocusInEvent foe) { 14985 if(tdh !is null && foe.target is tdh) 14986 tdh.redraw(); 14987 } 14988 14989 14990 /++ 14991 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. 14992 +/ 14993 void selectAll() { 14994 if(useCustomWidget) { 14995 tdh.selectAll(); 14996 } else version(win32_widgets) { 14997 SendMessage(hwnd, EM_SETSEL, 0, -1); 14998 } 14999 } 15000 15001 /++ 15002 Basic clipboard operations. 15003 15004 History: 15005 Added December 31, 2024 15006 +/ 15007 void copy() { 15008 if(useCustomWidget) { 15009 tdh.copy(); 15010 } else version(win32_widgets) { 15011 SendMessage(hwnd, WM_COPY, 0, 0); 15012 } 15013 } 15014 15015 /// ditto 15016 void cut() { 15017 if(useCustomWidget) { 15018 tdh.cut(); 15019 } else version(win32_widgets) { 15020 SendMessage(hwnd, WM_CUT, 0, 0); 15021 } 15022 } 15023 15024 /// ditto 15025 void paste() { 15026 if(useCustomWidget) { 15027 tdh.paste(); 15028 } else version(win32_widgets) { 15029 SendMessage(hwnd, WM_PASTE, 0, 0); 15030 } 15031 } 15032 15033 /// 15034 void undo() { 15035 if(useCustomWidget) { 15036 tdh.undo(); 15037 } else version(win32_widgets) { 15038 SendMessage(hwnd, EM_UNDO, 0, 0); 15039 } 15040 } 15041 15042 // note that WM_CLEAR deletes the selection without copying it to the clipboard 15043 // also windows supports margins, modified flag, and much more 15044 15045 // EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here 15046 15047 // EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc. 15048 15049 15050 15051 /*protected*/ TextDisplayHelper tdh; 15052 /*protected*/ TextLayouter textLayout; 15053 15054 /++ 15055 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. 15056 +/ 15057 @property string content() { 15058 if(useCustomWidget) { 15059 return textLayout.getTextString(); 15060 } else version(win32_widgets) { 15061 wchar[4096] bufferstack; 15062 wchar[] buffer; 15063 auto len = GetWindowTextLength(hwnd); 15064 if(len < bufferstack.length) 15065 buffer = bufferstack[0 .. len + 1]; 15066 else 15067 buffer = new wchar[](len + 1); 15068 15069 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 15070 if(l >= 0) 15071 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 15072 else 15073 return null; 15074 } 15075 15076 assert(0); 15077 } 15078 /// ditto 15079 @property void content(string s) { 15080 if(useCustomWidget) { 15081 with(textLayout.selection) { 15082 moveToStartOfDocument(); 15083 setAnchor(); 15084 moveToEndOfDocument(); 15085 setFocus(); 15086 replaceContent(s); 15087 } 15088 15089 tdh.adjustScrollbarSizes(); 15090 // these don't seem to help 15091 // tdh.smw.setPosition(0, 0); 15092 // tdh.scrollForCaret(); 15093 15094 redraw(); 15095 } else version(win32_widgets) { 15096 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 15097 SetWindowTextW(hwnd, bfr.ptr); 15098 } 15099 } 15100 15101 /++ 15102 Appends some text to the widget at the end, without affecting the user selection or cursor position. 15103 +/ 15104 void addText(string txt) { 15105 if(useCustomWidget) { 15106 textLayout.appendText(txt); 15107 tdh.adjustScrollbarSizes(); 15108 redraw(); 15109 } else version(win32_widgets) { 15110 // get the current selection 15111 DWORD StartPos, EndPos; 15112 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 15113 15114 // move the caret to the end of the text 15115 int outLength = GetWindowTextLengthW(hwnd); 15116 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 15117 15118 // insert the text at the new caret position 15119 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 15120 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 15121 15122 // restore the previous selection 15123 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 15124 } 15125 } 15126 15127 // EM_SCROLLCARET scrolls the caret into view 15128 15129 void scrollToBottom() { 15130 if(useCustomWidget) { 15131 tdh.smw.scrollDown(int.max); 15132 } else version(win32_widgets) { 15133 SendMessageW( hwnd, EM_LINESCROLL, 0, int.max ); 15134 } 15135 } 15136 15137 protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15138 return new TextDisplayHelper(textLayout, smw); 15139 } 15140 15141 protected TextStyle defaultTextStyle() { 15142 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 15143 } 15144 15145 private OperatingSystemFont getUsedFont() { 15146 auto cs = getComputedStyle(); 15147 auto font = cs.font; 15148 if(font is null) { 15149 font = new OperatingSystemFont; 15150 font.loadDefault(); 15151 } 15152 return font; 15153 } 15154 15155 protected void setupCustomTextEditing() { 15156 textLayout = new TextLayouter(defaultTextStyle()); 15157 15158 auto smw = new ScrollMessageWidget(this); 15159 if(!showingHorizontalScroll) 15160 smw.horizontalScrollBar.hide(); 15161 if(!showingVerticalScroll) 15162 smw.verticalScrollBar.hide(); 15163 this.tabStop = false; 15164 smw.tabStop = false; 15165 tdh = textDisplayHelperFactory(textLayout, smw); 15166 } 15167 15168 override void newParentWindow(Window old, Window n) { 15169 if(n is null) return; 15170 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 15171 if(textLayout) { 15172 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 15173 // the dpi change can change the font, so this informs the layouter that it has changed too 15174 style.font_ = getUsedFont(); 15175 15176 // arsd.core.writeln(this.parentWindow.win.actualDpi); 15177 } 15178 } 15179 }); 15180 } 15181 15182 static class Style : Widget.Style { 15183 override WidgetBackground background() { 15184 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 15185 } 15186 15187 override Color foregroundColor() { 15188 return WidgetPainter.visualTheme.foregroundColor; 15189 } 15190 15191 override FrameStyle borderStyle() { 15192 return FrameStyle.sunk; 15193 } 15194 15195 override MouseCursor cursor() { 15196 return GenericCursor.Text; 15197 } 15198 } 15199 mixin OverrideStyle!Style; 15200 15201 version(win32_widgets) { 15202 private string lastContentBlur; 15203 15204 override void defaultEventHandler_blur(BlurEvent ev) { 15205 super.defaultEventHandler_blur(ev); 15206 15207 if(!useCustomWidget) 15208 if(this.content != lastContentBlur) { 15209 auto evt = new ChangeEvent!string(this, &this.content); 15210 evt.dispatch(); 15211 lastContentBlur = this.content; 15212 } 15213 } 15214 } 15215 15216 15217 bool showingVerticalScroll() { return true; } 15218 bool showingHorizontalScroll() { return true; } 15219 } 15220 15221 /++ 15222 A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`. 15223 15224 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. 15225 15226 See_Also: 15227 [PasswordEdit] for a `LineEdit` that obscures its input. 15228 15229 [TextEdit] for a multi-line plain text editor widget. 15230 15231 [TextLabel] for a single line piece of static text. 15232 15233 [TextDisplay] for a read-only display of a larger piece of plain text. 15234 +/ 15235 class LineEdit : EditableTextWidget { 15236 override bool showingVerticalScroll() { return false; } 15237 override bool showingHorizontalScroll() { return false; } 15238 15239 override int flexBasisWidth() { return 250; } 15240 override int widthShrinkiness() { return 10; } 15241 15242 /// 15243 this(Widget parent) { 15244 super(parent); 15245 version(win32_widgets) { 15246 createWin32Window(this, "edit"w, "", 15247 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 15248 } else version(custom_widgets) { 15249 } else static assert(false); 15250 } 15251 15252 private this(bool useCustomWidget, Widget parent) { 15253 if(!useCustomWidget) 15254 this(parent); 15255 else 15256 super(true, parent); 15257 } 15258 15259 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15260 auto tdh = new TextDisplayHelper(textLayout, smw); 15261 tdh.singleLine = true; 15262 return tdh; 15263 } 15264 15265 version(win32_widgets) { 15266 mixin Padding!q{0}; 15267 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 15268 override int maxHeight() { return minHeight; } 15269 } 15270 15271 /+ 15272 @property void passwordMode(bool p) { 15273 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 15274 } 15275 +/ 15276 } 15277 15278 /// ditto 15279 class CustomLineEdit : LineEdit { 15280 this(Widget parent) { 15281 super(true, parent); 15282 } 15283 } 15284 15285 /++ 15286 A [LineEdit] that displays `*` in place of the actual characters. 15287 15288 Alas, Windows requires the window to be created differently to use this style, 15289 so it had to be a new class instead of a toggle on and off on an existing object. 15290 15291 History: 15292 Added January 24, 2021 15293 15294 Implemented on Linux on January 31, 2023. 15295 +/ 15296 class PasswordEdit : EditableTextWidget { 15297 override bool showingVerticalScroll() { return false; } 15298 override bool showingHorizontalScroll() { return false; } 15299 15300 override int flexBasisWidth() { return 250; } 15301 15302 override TextStyle defaultTextStyle() { 15303 auto cs = getComputedStyle(); 15304 15305 auto osf = new class OperatingSystemFont { 15306 this() { 15307 super(cs.font); 15308 } 15309 override fnum stringWidth(scope const(char)[] text, SimpleWindow window = null) { 15310 int count = 0; 15311 foreach(dchar ch; text) 15312 count++; 15313 return count * super.stringWidth("*", window); 15314 } 15315 }; 15316 15317 return new TextDisplayHelper.MyTextStyle(osf); 15318 } 15319 15320 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15321 static class TDH : TextDisplayHelper { 15322 this(TextLayouter textLayout, ScrollMessageWidget smw) { 15323 singleLine = true; 15324 super(textLayout, smw); 15325 } 15326 15327 override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 15328 char[256] buffer = void; 15329 int bufferLength = 0; 15330 foreach(dchar ch; text) 15331 buffer[bufferLength++] = '*'; 15332 painter.setFont(myStyle.font); 15333 painter.drawText(upperLeft, buffer[0..bufferLength]); 15334 } 15335 } 15336 15337 return new TDH(textLayout, smw); 15338 } 15339 15340 /// 15341 this(Widget parent) { 15342 super(parent); 15343 version(win32_widgets) { 15344 createWin32Window(this, "edit"w, "", 15345 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 15346 } else version(custom_widgets) { 15347 } else static assert(false); 15348 } 15349 15350 private this(bool useCustomWidget, Widget parent) { 15351 if(!useCustomWidget) 15352 this(parent); 15353 else 15354 super(true, parent); 15355 } 15356 15357 version(win32_widgets) { 15358 mixin Padding!q{2}; 15359 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 15360 override int maxHeight() { return minHeight; } 15361 } 15362 } 15363 15364 /// ditto 15365 class CustomPasswordEdit : PasswordEdit { 15366 this(Widget parent) { 15367 super(true, parent); 15368 } 15369 } 15370 15371 15372 /++ 15373 A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`. 15374 15375 See_Also: 15376 [TextDisplay] for a read-only text display. 15377 15378 [LineEdit] for a single line text editor. 15379 15380 [PasswordEdit] for a single line text editor that obscures its input. 15381 +/ 15382 class TextEdit : EditableTextWidget { 15383 /// 15384 this(Widget parent) { 15385 super(parent); 15386 version(win32_widgets) { 15387 createWin32Window(this, "edit"w, "", 15388 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 15389 } else version(custom_widgets) { 15390 } else static assert(false); 15391 } 15392 15393 private this(bool useCustomWidget, Widget parent) { 15394 if(!useCustomWidget) 15395 this(parent); 15396 else 15397 super(true, parent); 15398 } 15399 15400 override int maxHeight() { return int.max; } 15401 override int heightStretchiness() { return 7; } 15402 15403 override int flexBasisWidth() { return 250; } 15404 override int flexBasisHeight() { return 25; } 15405 } 15406 15407 /// ditto 15408 class CustomTextEdit : TextEdit { 15409 this(Widget parent) { 15410 super(true, parent); 15411 } 15412 } 15413 15414 /+ 15415 /++ 15416 15417 +/ 15418 version(none) 15419 class RichTextDisplay : Widget { 15420 @property void content(string c) {} 15421 void appendContent(string c) {} 15422 } 15423 +/ 15424 15425 /++ 15426 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. 15427 15428 History: 15429 Added October 31, 2023 (dub v11.3) 15430 +/ 15431 class TextDisplay : EditableTextWidget { 15432 this(string text, Widget parent) { 15433 super(true, parent); 15434 this.content = text; 15435 } 15436 15437 override int maxHeight() { return int.max; } 15438 override int minHeight() { return Window.defaultLineHeight; } 15439 override int heightStretchiness() { return 7; } 15440 override int heightShrinkiness() { return 2; } 15441 15442 override int flexBasisWidth() { 15443 return scaleWithDpi(250); 15444 } 15445 override int flexBasisHeight() { 15446 if(textLayout is null || this.tdh is null) 15447 return Window.defaultLineHeight; 15448 15449 auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height; 15450 return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height; 15451 } 15452 15453 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15454 return new MyTextDisplayHelper(textLayout, smw); 15455 } 15456 15457 override void registerMovement() { 15458 super.registerMovement(); 15459 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 15460 } 15461 15462 static class MyTextDisplayHelper : TextDisplayHelper { 15463 this(TextLayouter textLayout, ScrollMessageWidget smw) { 15464 smw.verticalScrollBar.hide(); 15465 smw.horizontalScrollBar.hide(); 15466 super(textLayout, smw); 15467 this.readonly = true; 15468 } 15469 15470 override void registerMovement() { 15471 super.registerMovement(); 15472 15473 // FIXME: do the horizontal one too as needed and make sure that it does 15474 // wordwrapping again 15475 if(l.height + smw.horizontalScrollBar.height > this.height) 15476 smw.verticalScrollBar.show(); 15477 else 15478 smw.verticalScrollBar.hide(); 15479 15480 l.wordWrapWidth = this.width; 15481 15482 smw.verticalScrollBar.setPosition = 0; 15483 } 15484 } 15485 15486 static class Style : Widget.Style { 15487 // just want the generic look for these 15488 } 15489 15490 mixin OverrideStyle!Style; 15491 } 15492 15493 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 15494 /++ 15495 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. 15496 15497 15498 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 15499 15500 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 15501 15502 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. 15503 15504 History: 15505 Added August 12, 2024 (dub v11.6) 15506 +/ 15507 abstract class GenericListViewWidget : Widget { 15508 /++ 15509 15510 +/ 15511 this(Widget parent) { 15512 super(parent); 15513 15514 smw = new ScrollMessageWidget(this); 15515 smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width); 15516 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 15517 smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet 15518 15519 inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw)); 15520 inner.tabStop = this.tabStop; 15521 this.tabStop = false; 15522 } 15523 15524 private ScrollMessageWidget smw; 15525 private GenericListViewWidgetInner inner; 15526 15527 /++ 15528 15529 +/ 15530 abstract GenericListViewItem itemFactory(Widget parent); 15531 // in device-dependent pixels 15532 /++ 15533 15534 +/ 15535 abstract Size itemSize(); // use 0 to indicate it can stretch? 15536 15537 enum LayoutMode { 15538 rows, 15539 columns, 15540 gridRowsFirst, 15541 gridColumnsFirst 15542 } 15543 LayoutMode layoutMode() { 15544 return LayoutMode.rows; 15545 } 15546 15547 private int itemCount_; 15548 15549 /++ 15550 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. 15551 +/ 15552 void setItemCount(int count) { 15553 smw.setTotalArea(inner.width, count * itemSize().height); 15554 smw.setViewableArea(inner.width, inner.height); 15555 this.itemCount_ = count; 15556 } 15557 15558 /++ 15559 Returns the current count of items expected to available in the list. 15560 +/ 15561 int itemCount() { 15562 return this.itemCount_; 15563 } 15564 15565 /++ 15566 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 15567 15568 Note you must $(I also) call [setItemCount] if the total item count has changed. 15569 +/ 15570 void notifyItemsChanged(int index, int count = 1) { 15571 } 15572 /// ditto 15573 void notifyItemsInserted(int index, int count = 1) { 15574 } 15575 /// ditto 15576 void notifyItemsRemoved(int index, int count = 1) { 15577 } 15578 /// ditto 15579 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 15580 } 15581 15582 /++ 15583 History: 15584 Added January 1, 2025 15585 +/ 15586 void ensureItemVisibleInScroll(int index) { 15587 auto itemPos = index * itemSize().height; 15588 auto vsb = smw.verticalScrollBar; 15589 auto viewable = vsb.viewableArea_; 15590 15591 if(viewable == 0) { 15592 // viewable == 0 isn't actually supposed to happen, this means 15593 // this method is being called before having our size assigned, it should 15594 // probably just queue it up for later. 15595 queuedScroll = index; 15596 return; 15597 } 15598 15599 queuedScroll = int.min; 15600 15601 if(itemPos < vsb.position) { 15602 // scroll up to it 15603 vsb.setPosition(itemPos); 15604 smw.notify(); 15605 } else if(itemPos + itemSize().height > (vsb.position + viewable)) { 15606 // scroll down to it, so it is at the bottom 15607 15608 auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height; 15609 // need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it 15610 15611 vsb.setPosition(itemPos - lastViewableItemPosition); 15612 smw.notify(); 15613 } 15614 } 15615 15616 /++ 15617 History: 15618 Added January 1, 2025; 15619 +/ 15620 int numberOfCurrentlyFullyVisibleItems() { 15621 return smw.verticalScrollBar.viewableArea_ / itemSize.height; 15622 } 15623 15624 private int queuedScroll = int.min; 15625 15626 override void recomputeChildLayout() { 15627 super.recomputeChildLayout(); 15628 if(queuedScroll != int.min) 15629 ensureItemVisibleInScroll(queuedScroll); 15630 } 15631 15632 private GenericListViewItem[] items; 15633 15634 override void paint(WidgetPainter painter) {} 15635 } 15636 15637 /// ditto 15638 abstract class GenericListViewItem : Widget { 15639 /++ 15640 +/ 15641 this(Widget parent) { 15642 super(parent); 15643 } 15644 15645 private int _currentIndex = -1; 15646 15647 private void showItemPrivate(int idx) { 15648 showItem(idx); 15649 _currentIndex = idx; 15650 } 15651 15652 /++ 15653 Implement this to show an item from your data backing to the list. 15654 15655 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. 15656 +/ 15657 abstract void showItem(int idx); 15658 15659 /++ 15660 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 15661 15662 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 15663 15664 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. 15665 +/ 15666 final int currentIndexLoaded() { 15667 return _currentIndex; 15668 } 15669 } 15670 15671 /// 15672 unittest { 15673 import arsd.minigui; 15674 15675 import std.conv; 15676 15677 void main() { 15678 auto mw = new MainWindow(); 15679 15680 static class MyListViewItem : GenericListViewItem { 15681 this(Widget parent) { 15682 super(parent); 15683 15684 label = new TextLabel("unloaded", TextAlignment.Left, this); 15685 button = new Button("Click", this); 15686 15687 button.addEventListener("triggered", (){ 15688 messageBox(text("clicked ", currentIndexLoaded())); 15689 }); 15690 } 15691 override void showItem(int idx) { 15692 label.label = "Item " ~ to!string(idx); 15693 } 15694 15695 TextLabel label; 15696 Button button; 15697 } 15698 15699 auto widget = new class GenericListViewWidget { 15700 this() { 15701 super(mw); 15702 } 15703 override GenericListViewItem itemFactory(Widget parent) { 15704 return new MyListViewItem(parent); 15705 } 15706 override Size itemSize() { 15707 return Size(0, scaleWithDpi(80)); 15708 } 15709 }; 15710 15711 widget.setItemCount(5000); 15712 15713 mw.loop(); 15714 } 15715 } 15716 15717 // this exists just to wrap the actual GenericListViewWidgetInner so borders 15718 // and padding and stuff can work 15719 private class GenericListViewInnerContainer : Widget { 15720 this(Widget parent) { 15721 super(parent); 15722 this.tabStop = false; 15723 } 15724 15725 override void recomputeChildLayout() { 15726 registerMovement(); 15727 15728 auto cs = getComputedStyle(); 15729 auto bw = getBorderWidth(cs.borderStyle); 15730 15731 assert(children.length < 2); 15732 foreach(child; children) { 15733 child.x = bw + paddingLeft(); 15734 child.y = bw + paddingTop(); 15735 child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight(); 15736 child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom(); 15737 15738 child.recomputeChildLayout(); 15739 } 15740 } 15741 15742 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 15743 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15744 return parent.parent.parent.useStyleProperties(dg); 15745 else 15746 return super.useStyleProperties(dg); 15747 } 15748 15749 override int paddingTop() { 15750 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15751 return parent.parent.parent.paddingTop(); 15752 else 15753 return super.paddingTop(); 15754 } 15755 15756 override int paddingBottom() { 15757 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15758 return parent.parent.parent.paddingBottom(); 15759 else 15760 return super.paddingBottom(); 15761 } 15762 15763 override int paddingLeft() { 15764 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15765 return parent.parent.parent.paddingLeft(); 15766 else 15767 return super.paddingLeft(); 15768 } 15769 15770 override int paddingRight() { 15771 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15772 return parent.parent.parent.paddingRight(); 15773 else 15774 return super.paddingRight(); 15775 } 15776 15777 15778 } 15779 15780 private class GenericListViewWidgetInner : Widget { 15781 this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) { 15782 super(parent); 15783 this.glvw = glvw; 15784 15785 reloadVisible(); 15786 15787 smw.addEventListener("scroll", () { 15788 reloadVisible(); 15789 }); 15790 } 15791 15792 override void registerMovement() { 15793 super.registerMovement(); 15794 if(glvw && glvw.smw) 15795 glvw.smw.setViewableArea(this.width, this.height); 15796 } 15797 15798 void reloadVisible() { 15799 auto y = glvw.smw.position.y / glvw.itemSize.height; 15800 15801 // idk why i had this here it doesn't seem to be ueful and actually made last items diasppear 15802 //int offset = glvw.smw.position.y % glvw.itemSize.height; 15803 //if(offset || y >= glvw.itemCount()) 15804 //y--; 15805 15806 if(y < 0) 15807 y = 0; 15808 15809 recomputeChildLayout(); 15810 15811 foreach(item; glvw.items) { 15812 if(y < glvw.itemCount()) { 15813 item.showItemPrivate(y); 15814 item.show(); 15815 } else { 15816 item.hide(); 15817 } 15818 y++; 15819 } 15820 15821 this.redraw(); 15822 } 15823 15824 private GenericListViewWidget glvw; 15825 15826 private bool inRcl; 15827 override void recomputeChildLayout() { 15828 if(inRcl) 15829 return; 15830 inRcl = true; 15831 scope(exit) 15832 inRcl = false; 15833 15834 registerMovement(); 15835 15836 auto ih = glvw.itemSize().height; 15837 15838 auto itemCount = this.height / ih + 2; // extra for partial display before and after 15839 bool hadNew; 15840 while(glvw.items.length < itemCount) { 15841 // FIXME: free the old items? maybe just set length 15842 glvw.items ~= glvw.itemFactory(this); 15843 hadNew = true; 15844 } 15845 15846 if(hadNew) 15847 reloadVisible(); 15848 15849 int y = -(glvw.smw.position.y % ih) + this.paddingTop(); 15850 foreach(child; children) { 15851 child.x = this.paddingLeft(); 15852 child.y = y; 15853 y += glvw.itemSize().height; 15854 child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight(); 15855 child.height = ih; 15856 15857 child.recomputeChildLayout(); 15858 } 15859 } 15860 } 15861 15862 15863 15864 /++ 15865 History: 15866 It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`. 15867 +/ 15868 class MessageBox : Dialog { 15869 private string message; 15870 MessageBoxButton buttonPressed = MessageBoxButton.None; 15871 /++ 15872 15873 History: 15874 The overload that takes `Window originator` was added on September 29, 2024. 15875 +/ 15876 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15877 this(null, message, buttons, buttonIds); 15878 } 15879 /// ditto 15880 this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15881 message = message.stripRightInternal; 15882 int mainWidth; 15883 15884 // estimate longest line 15885 int count; 15886 foreach(ch; message) { 15887 if(ch == '\n') { 15888 if(count > mainWidth) 15889 mainWidth = count; 15890 count = 0; 15891 } else { 15892 count++; 15893 } 15894 } 15895 mainWidth *= 8; 15896 if(mainWidth < 300) 15897 mainWidth = 300; 15898 if(mainWidth > 600) 15899 mainWidth = 600; 15900 15901 super(originator, mainWidth, 100); 15902 15903 assert(buttons.length); 15904 assert(buttons.length == buttonIds.length); 15905 15906 this.message = message; 15907 15908 auto label = new TextDisplay(message, this); 15909 15910 auto hl = new HorizontalLayout(this); 15911 auto spacer = new HorizontalSpacer(hl); // to right align 15912 15913 foreach(idx, buttonText; buttons) { 15914 auto button = new CommandButton(buttonText, hl); 15915 15916 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 15917 this.buttonPressed = buttonIds[idx]; 15918 win.close(); 15919 }; })(idx)); 15920 15921 if(idx == 0) 15922 button.focus(); 15923 } 15924 15925 if(buttons.length == 1) 15926 auto spacer2 = new HorizontalSpacer(hl); // to center it 15927 15928 auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom; 15929 auto max = scaleWithDpi(600); // random max height 15930 if(size > max) 15931 size = max; 15932 15933 win.resize(scaleWithDpi(mainWidth), size); 15934 15935 win.show(); 15936 redraw(); 15937 } 15938 15939 override void OK() { 15940 this.win.close(); 15941 } 15942 15943 mixin Padding!q{16}; 15944 } 15945 15946 /// 15947 enum MessageBoxStyle { 15948 OK, /// 15949 OKCancel, /// 15950 RetryCancel, /// 15951 YesNo, /// 15952 YesNoCancel, /// 15953 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. 15954 } 15955 15956 /// 15957 enum MessageBoxIcon { 15958 None, /// 15959 Info, /// 15960 Warning, /// 15961 Error /// 15962 } 15963 15964 /// Identifies the button the user pressed on a message box. 15965 enum MessageBoxButton { 15966 None, /// The user closed the message box without clicking any of the buttons. 15967 OK, /// 15968 Cancel, /// 15969 Retry, /// 15970 Yes, /// 15971 No, /// 15972 Continue /// 15973 } 15974 15975 15976 /++ 15977 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. 15978 15979 Returns: the button pressed. 15980 +/ 15981 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15982 return messageBox(null, title, message, style, icon); 15983 } 15984 15985 /// ditto 15986 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15987 return messageBox(null, null, message, style, icon); 15988 } 15989 15990 /++ 15991 15992 +/ 15993 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15994 version(win32_widgets) { 15995 WCharzBuffer t = WCharzBuffer(title); 15996 WCharzBuffer m = WCharzBuffer(message); 15997 UINT type; 15998 with(MessageBoxStyle) 15999 final switch(style) { 16000 case OK: type |= MB_OK; break; 16001 case OKCancel: type |= MB_OKCANCEL; break; 16002 case RetryCancel: type |= MB_RETRYCANCEL; break; 16003 case YesNo: type |= MB_YESNO; break; 16004 case YesNoCancel: type |= MB_YESNOCANCEL; break; 16005 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 16006 } 16007 with(MessageBoxIcon) 16008 final switch(icon) { 16009 case None: break; 16010 case Info: type |= MB_ICONINFORMATION; break; 16011 case Warning: type |= MB_ICONWARNING; break; 16012 case Error: type |= MB_ICONERROR; break; 16013 } 16014 switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) { 16015 case IDOK: return MessageBoxButton.OK; 16016 case IDCANCEL: return MessageBoxButton.Cancel; 16017 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 16018 case IDYES: return MessageBoxButton.Yes; 16019 case IDNO: return MessageBoxButton.No; 16020 case IDCONTINUE: return MessageBoxButton.Continue; 16021 default: return MessageBoxButton.None; 16022 } 16023 } else { 16024 string[] buttons; 16025 MessageBoxButton[] buttonIds; 16026 with(MessageBoxStyle) 16027 final switch(style) { 16028 case OK: 16029 buttons = ["OK"]; 16030 buttonIds = [MessageBoxButton.OK]; 16031 break; 16032 case OKCancel: 16033 buttons = ["OK", "Cancel"]; 16034 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 16035 break; 16036 case RetryCancel: 16037 buttons = ["Retry", "Cancel"]; 16038 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 16039 break; 16040 case YesNo: 16041 buttons = ["Yes", "No"]; 16042 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 16043 break; 16044 case YesNoCancel: 16045 buttons = ["Yes", "No", "Cancel"]; 16046 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 16047 break; 16048 case RetryCancelContinue: 16049 buttons = ["Try Again", "Cancel", "Continue"]; 16050 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 16051 break; 16052 } 16053 auto mb = new MessageBox(originator, message, buttons, buttonIds); 16054 EventLoop el = EventLoop.get; 16055 el.run(() { return !mb.win.closed; }); 16056 return mb.buttonPressed; 16057 } 16058 16059 } 16060 16061 /// ditto 16062 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 16063 return messageBox(originator, null, message, style, icon); 16064 } 16065 16066 16067 /// 16068 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 16069 16070 /++ 16071 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 16072 16073 History: 16074 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. 16075 +/ 16076 struct EventListener { 16077 private Widget widget; 16078 private string event; 16079 private EventHandler handler; 16080 private bool useCapture; 16081 16082 /// 16083 void disconnect() { 16084 if(widget !is null && handler !is null) 16085 widget.removeEventListener(this); 16086 } 16087 } 16088 16089 /++ 16090 The purpose of this enum was to give a compile-time checked version of various standard event strings. 16091 16092 Now, I recommend you use a statically typed event object instead. 16093 16094 See_Also: [Event] 16095 +/ 16096 enum EventType : string { 16097 click = "click", /// 16098 16099 mouseenter = "mouseenter", /// 16100 mouseleave = "mouseleave", /// 16101 mousein = "mousein", /// 16102 mouseout = "mouseout", /// 16103 mouseup = "mouseup", /// 16104 mousedown = "mousedown", /// 16105 mousemove = "mousemove", /// 16106 16107 keydown = "keydown", /// 16108 keyup = "keyup", /// 16109 char_ = "char", /// 16110 16111 focus = "focus", /// 16112 blur = "blur", /// 16113 16114 triggered = "triggered", /// 16115 16116 change = "change", /// 16117 } 16118 16119 /++ 16120 Represents an event that is currently being processed. 16121 16122 16123 Minigui's event model is based on the web browser. An event has a name, a target, 16124 and an associated data object. It starts from the window and works its way down through 16125 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 16126 then goes back up again all the way back to the window, triggering bubble phase handlers. At 16127 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 16128 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 16129 was called; that just stops it from calling other handlers in the widget tree, but the default happens 16130 whenever propagation is done, not only if it gets to the end of the chain). 16131 16132 This model has several nice points: 16133 16134 $(LIST 16135 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 16136 with event handlers set, then add/remove children as much as you want without needing 16137 to manage the event handlers on them - the parent alone can manage everything. 16138 16139 * It is easy to create new custom events in your application. 16140 16141 * It is familiar to many web developers. 16142 ) 16143 16144 There's a few downsides though: 16145 16146 $(LIST 16147 * There's not a lot of type safety. 16148 16149 * You don't get a static list of what events a widget can emit. 16150 16151 * Tracing where an event got cancelled along the chain can get difficult; the downside of 16152 the central delegation benefit is it can be lead to debugging of action at a distance. 16153 ) 16154 16155 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 16156 while keeping the benefits - and most compatibility with - the existing model. The main idea is 16157 to simply use a D object type which provides a static interface as well as a built-in event name. 16158 Then, a new static interface allows you to see what an event can emit and attach handlers to it 16159 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 16160 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 16161 to having a little more help from the D compiler and documentation generator. 16162 16163 Your code would change like this: 16164 16165 --- 16166 // old 16167 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 16168 16169 // new 16170 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 16171 --- 16172 16173 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. 16174 16175 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 16176 16177 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 16178 16179 Thus the family of functions are: 16180 16181 [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. 16182 16183 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 16184 16185 [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. 16186 16187 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 16188 16189 --- 16190 class MyCheckbox : Widget { 16191 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 16192 /// It is NOT actually required but should be used whenever possible. 16193 mixin Emits!(ChangeEvent!bool); 16194 16195 this(Widget parent) { 16196 super(parent); 16197 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 16198 } 16199 16200 private bool _checked; 16201 @property bool checked() { return _checked; } 16202 @property void checked(bool set) { 16203 _checked = set; 16204 emit!(ChangeEvent!bool)(&checked); 16205 } 16206 } 16207 --- 16208 16209 ## Creating Your Own Events 16210 16211 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. 16212 16213 --- 16214 final class MyEvent : Event { 16215 this(Widget target) { super(EventString, target); } 16216 mixin Register; // adds EventString and other reflection information 16217 } 16218 --- 16219 16220 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 16221 16222 History: 16223 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. 16224 16225 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. 16226 +/ 16227 /+ 16228 16229 ## General Conventions 16230 16231 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. 16232 16233 16234 ## Qt-style signals and slots 16235 16236 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. 16237 16238 The intention is for events to be used when 16239 16240 --- 16241 class Demo : Widget { 16242 this() { 16243 myPropertyChanged = Signal!int(this); 16244 } 16245 @property myProperty(int v) { 16246 myPropertyChanged.emit(v); 16247 } 16248 16249 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 16250 // but it can just genuinely not care about `this` since that's not really passed. 16251 } 16252 16253 class Foo : Widget { 16254 // the slot uda is not necessary, but it helps the script and ui builder find it. 16255 @slot void setValue(int v) { ... } 16256 } 16257 16258 demo.myPropertyChanged.connect(&foo.setValue); 16259 --- 16260 16261 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 16262 16263 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 16264 16265 class StringChangeEvent : ChangeEvent, Signal!string { 16266 mixin SignalImpl 16267 } 16268 16269 +/ 16270 class Event : ReflectableProperties { 16271 /// Creates an event without populating any members and without sending it. See [dispatch] 16272 this(string eventName, Widget emittedBy) { 16273 this.eventName = eventName; 16274 this.srcElement = emittedBy; 16275 } 16276 16277 16278 /// Implementations for the [ReflectableProperties] interface/ 16279 void getPropertiesList(scope void delegate(string name) sink) const {} 16280 /// ditto 16281 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 16282 /// ditto 16283 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 16284 return SetPropertyResult.notPermitted; 16285 } 16286 16287 16288 /+ 16289 /++ 16290 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 16291 16292 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. 16293 +/ 16294 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 16295 if(value.length == 0) { 16296 finalSink(memberName, `""`); 16297 return; 16298 } 16299 16300 char[1024] bufferBacking; 16301 char[] buffer = bufferBacking; 16302 int bufferPosition; 16303 16304 void sink(char ch) { 16305 if(bufferPosition >= buffer.length) 16306 buffer.length = buffer.length + 1024; 16307 buffer[bufferPosition++] = ch; 16308 } 16309 16310 sink('"'); 16311 16312 foreach(ch; value) { 16313 switch(ch) { 16314 case '\\': 16315 sink('\\'); sink('\\'); 16316 break; 16317 case '"': 16318 sink('\\'); sink('"'); 16319 break; 16320 case '\n': 16321 sink('\\'); sink('n'); 16322 break; 16323 case '\r': 16324 sink('\\'); sink('r'); 16325 break; 16326 case '\t': 16327 sink('\\'); sink('t'); 16328 break; 16329 default: 16330 sink(ch); 16331 } 16332 } 16333 16334 sink('"'); 16335 16336 finalSink(memberName, buffer[0 .. bufferPosition]); 16337 } 16338 +/ 16339 16340 /+ 16341 enum EventInitiator { 16342 system, 16343 minigui, 16344 user 16345 } 16346 16347 immutable EventInitiator; initiatedBy; 16348 +/ 16349 16350 /++ 16351 Events should generally follow the propagation model, but there's some exceptions 16352 to that rule. If so, they should override this to return false. In that case, only 16353 bubbling event handlers on the target itself and capturing event handlers on the containing 16354 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 16355 capture -> target -> bubble process.) 16356 16357 History: 16358 Added May 12, 2021 16359 +/ 16360 bool propagates() const pure nothrow @nogc @safe { 16361 return true; 16362 } 16363 16364 /++ 16365 hints as to whether preventDefault will actually do anything. not entirely reliable. 16366 16367 History: 16368 Added May 14, 2021 16369 +/ 16370 bool cancelable() const pure nothrow @nogc @safe { 16371 return true; 16372 } 16373 16374 /++ 16375 You can mix this into child class to register some boilerplate. It includes the `EventString` 16376 member, a constructor, and implementations of the dynamic get data interfaces. 16377 16378 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 16379 16380 16381 You can override the default EventString by simply providing your own in the form of 16382 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 16383 which provides some namespace protection against conflicts in other libraries while still being fairly 16384 easy to use. 16385 16386 If you provide your own constructor, it will override the default constructor provided here. A constructor 16387 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 16388 first argument to your constructor. 16389 16390 History: 16391 Added May 13, 2021. 16392 +/ 16393 protected static mixin template Register() { 16394 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 16395 this(Widget target) { super(EventString, target); } 16396 16397 mixin ReflectableProperties.RegisterGetters; 16398 } 16399 16400 /++ 16401 This is the widget that emitted the event. 16402 16403 16404 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 16405 16406 History: 16407 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 16408 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 16409 so I don't intend to remove these aliases. 16410 +/ 16411 Widget source; 16412 /// ditto 16413 alias source target; 16414 /// ditto 16415 alias source srcElement; 16416 16417 Widget relatedTarget; /// Note: likely to be deprecated at some point. 16418 16419 /// Prevents the default event handler (if there is one) from being called 16420 void preventDefault() { 16421 lastDefaultPrevented = true; 16422 defaultPrevented_ = true; 16423 } 16424 16425 /// Stops the event propagation immediately. 16426 void stopPropagation() { 16427 propagationStopped = true; 16428 } 16429 16430 private bool defaultPrevented_; 16431 public bool defaultPrevented() { 16432 return defaultPrevented_; 16433 } 16434 private bool propagationStopped; 16435 private string eventName; 16436 16437 private bool isBubbling; 16438 16439 /// 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. 16440 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 16441 16442 /++ 16443 this sends it only to the target. If you want propagation, use dispatch() instead. 16444 16445 This should be made private!!! 16446 16447 +/ 16448 void sendDirectly() { 16449 if(srcElement is null) 16450 return; 16451 16452 // 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. 16453 16454 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 16455 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 16456 16457 if(auto e = target.parentWindow) { 16458 if(auto handlers = "*" in e.capturingEventHandlers) 16459 foreach(handler; *handlers) 16460 if(handler) handler(e, this); 16461 if(auto handlers = eventName in e.capturingEventHandlers) 16462 foreach(handler; *handlers) 16463 if(handler) handler(e, this); 16464 } 16465 16466 auto e = srcElement; 16467 16468 if(auto handlers = eventName in e.bubblingEventHandlers) 16469 foreach(handler; *handlers) 16470 if(handler) handler(e, this); 16471 16472 if(auto handlers = "*" in e.bubblingEventHandlers) 16473 foreach(handler; *handlers) 16474 if(handler) handler(e, this); 16475 16476 // there's never a default for a catch-all event 16477 if(!defaultPrevented) 16478 if(eventName in e.defaultEventHandlers) 16479 e.defaultEventHandlers[eventName](e, this); 16480 } 16481 16482 /// this dispatches the element using the capture -> target -> bubble process 16483 void dispatch() { 16484 if(srcElement is null) 16485 return; 16486 16487 if(!propagates) { 16488 sendDirectly; 16489 return; 16490 } 16491 16492 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 16493 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 16494 16495 // first capture, then bubble 16496 16497 Widget[] chain; 16498 Widget curr = srcElement; 16499 while(curr) { 16500 auto l = curr; 16501 chain ~= l; 16502 curr = curr.parent; 16503 } 16504 16505 isBubbling = false; 16506 16507 foreach_reverse(e; chain) { 16508 if(auto handlers = "*" in e.capturingEventHandlers) 16509 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16510 16511 if(propagationStopped) 16512 break; 16513 16514 if(auto handlers = eventName in e.capturingEventHandlers) 16515 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16516 16517 // the default on capture should really be to always do nothing 16518 16519 //if(!defaultPrevented) 16520 // if(eventName in e.defaultEventHandlers) 16521 // e.defaultEventHandlers[eventName](e.element, this); 16522 16523 if(propagationStopped) 16524 break; 16525 } 16526 16527 int adjustX; 16528 int adjustY; 16529 16530 isBubbling = true; 16531 if(!propagationStopped) 16532 foreach(e; chain) { 16533 if(auto handlers = eventName in e.bubblingEventHandlers) 16534 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16535 16536 if(propagationStopped) 16537 break; 16538 16539 if(auto handlers = "*" in e.bubblingEventHandlers) 16540 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16541 16542 if(propagationStopped) 16543 break; 16544 16545 if(e.encapsulatedChildren()) { 16546 adjustClientCoordinates(adjustX, adjustY); 16547 target = e; 16548 } else { 16549 adjustX += e.x; 16550 adjustY += e.y; 16551 } 16552 } 16553 16554 if(!defaultPrevented) 16555 foreach(e; chain) { 16556 if(eventName in e.defaultEventHandlers) 16557 e.defaultEventHandlers[eventName](e, this); 16558 } 16559 } 16560 16561 16562 /* old compatibility things */ 16563 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 16564 final @property { 16565 Key key() { return (cast(KeyEventBase) this).key; } 16566 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 16567 16568 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 16569 bool altKey() { return (cast(KeyEventBase) this).altKey; } 16570 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 16571 } 16572 16573 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 16574 final @property { 16575 int clientX() { return (cast(MouseEventBase) this).clientX; } 16576 int clientY() { return (cast(MouseEventBase) this).clientY; } 16577 16578 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 16579 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 16580 16581 int button() { return (cast(MouseEventBase) this).button; } 16582 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 16583 } 16584 16585 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 16586 final @property { 16587 int state() { 16588 if(auto meb = cast(MouseEventBase) this) 16589 return meb.state; 16590 if(auto keb = cast(KeyEventBase) this) 16591 return keb.state; 16592 assert(0); 16593 } 16594 } 16595 16596 deprecated("Use a CharEvent instead of Event in your handler going forward") 16597 final @property { 16598 dchar character() { 16599 if(auto ce = cast(CharEvent) this) 16600 return ce.character; 16601 return dchar_invalid; 16602 } 16603 } 16604 16605 // for change events 16606 @property { 16607 /// 16608 int intValue() { return 0; } 16609 /// 16610 string stringValue() { return null; } 16611 } 16612 } 16613 16614 /++ 16615 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 16616 16617 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 16618 dynamic and custom events, but the static list helps ensure you get them right. 16619 16620 If this is declared, you can use [Widget.emit] to send the event. 16621 16622 All events work the same way though, following the capture->widget->bubble model described under [Event]. 16623 16624 History: 16625 Added May 4, 2021 16626 +/ 16627 mixin template Emits(EventType) { 16628 import arsd.minigui : EventString; 16629 static if(is(EventType : Event) && !is(EventType == Event)) 16630 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 16631 else 16632 static assert(0, "You can only emit subclasses of Event"); 16633 } 16634 16635 /// ditto 16636 mixin template Emits(string eventString) { 16637 mixin("private Event[0] emits_" ~ eventString ~";"); 16638 } 16639 16640 /* 16641 class SignalEvent(string name) : Event { 16642 16643 } 16644 */ 16645 16646 /++ 16647 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". 16648 16649 16650 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. 16651 16652 History: 16653 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 16654 +/ 16655 class CommandEvent : Event { 16656 enum EventString = "command"; 16657 this(Widget source, string CommandString = EventString) { 16658 super(CommandString, source); 16659 } 16660 } 16661 16662 /++ 16663 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 16664 +/ 16665 class CommandEventWithArgs(Args...) : CommandEvent { 16666 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 16667 Args args; 16668 } 16669 16670 /++ 16671 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. 16672 16673 See [CommandEvent] for more information. 16674 16675 Returns: 16676 The [EventListener] you can use to remove the handler. 16677 +/ 16678 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 16679 return w.addEventListener(CommandString, (Event ev) { 16680 if(ev.target is w) 16681 return; // it does not consume its own commands! 16682 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 16683 handler(cev.args); 16684 ev.stopPropagation(); 16685 } 16686 }); 16687 } 16688 16689 /++ 16690 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. 16691 +/ 16692 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 16693 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 16694 event.dispatch(); 16695 } 16696 16697 /++ 16698 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. 16699 16700 If you need to know the old size, you need to store it yourself. 16701 16702 History: 16703 Made final on January 3, 2025 (dub v12.0) 16704 +/ 16705 final class ResizeEvent : Event { 16706 enum EventString = "resize"; 16707 16708 this(Widget target) { super(EventString, target); } 16709 16710 override bool propagates() const { return false; } 16711 } 16712 16713 /++ 16714 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 16715 16716 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. 16717 16718 History: 16719 Added June 21, 2021 (dub v10.1) 16720 16721 Made final on January 3, 2025 (dub v12.0) 16722 +/ 16723 final class ClosingEvent : Event { 16724 enum EventString = "closing"; 16725 16726 this(Widget target) { super(EventString, target); } 16727 16728 override bool propagates() const { return false; } 16729 override bool cancelable() const { return true; } 16730 } 16731 16732 /// ditto 16733 final class ClosedEvent : Event { 16734 enum EventString = "closed"; 16735 16736 this(Widget target) { super(EventString, target); } 16737 16738 override bool propagates() const { return false; } 16739 override bool cancelable() const { return false; } 16740 } 16741 16742 /// 16743 final class BlurEvent : Event { 16744 enum EventString = "blur"; 16745 16746 // FIXME: related target? 16747 this(Widget target) { super(EventString, target); } 16748 16749 override bool propagates() const { return false; } 16750 } 16751 16752 /// 16753 final class FocusEvent : Event { 16754 enum EventString = "focus"; 16755 16756 // FIXME: related target? 16757 this(Widget target) { super(EventString, target); } 16758 16759 override bool propagates() const { return false; } 16760 } 16761 16762 /++ 16763 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 16764 16765 History: 16766 Added July 3, 2021 16767 +/ 16768 final class FocusInEvent : Event { 16769 enum EventString = "focusin"; 16770 16771 // FIXME: related target? 16772 this(Widget target) { super(EventString, target); } 16773 16774 override bool cancelable() const { return false; } 16775 } 16776 16777 /// ditto 16778 final class FocusOutEvent : Event { 16779 enum EventString = "focusout"; 16780 16781 // FIXME: related target? 16782 this(Widget target) { super(EventString, target); } 16783 16784 override bool cancelable() const { return false; } 16785 } 16786 16787 /// 16788 final class ScrollEvent : Event { 16789 enum EventString = "scroll"; 16790 this(Widget target) { super(EventString, target); } 16791 16792 override bool cancelable() const { return false; } 16793 } 16794 16795 /++ 16796 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 16797 16798 History: 16799 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 16800 +/ 16801 final class CharEvent : Event { 16802 enum EventString = "char"; 16803 this(Widget target, dchar ch) { 16804 character = ch; 16805 super(EventString, target); 16806 } 16807 16808 immutable dchar character; 16809 } 16810 16811 /++ 16812 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 16813 +/ 16814 abstract class ChangeEventBase : Event { 16815 enum EventString = "change"; 16816 this(Widget target) { 16817 super(EventString, target); 16818 } 16819 16820 /+ 16821 // idk where or how exactly i want to do this. 16822 // i might come back to it later. 16823 16824 // If a widget itself broadcasts one of theses itself, it stops propagation going down 16825 // this way the source doesn't get too confused (think of a nested scroll widget) 16826 // 16827 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 16828 // then you consume that command and change you scroll x position to whatever. then you do 16829 // some kind of change event that is broadcast back to the children and any horizontal scroll 16830 // listeners are now able to update, without having an explicit connection between them. 16831 void broadcastToChildren(string fieldName) { 16832 16833 } 16834 +/ 16835 } 16836 16837 /++ 16838 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. 16839 16840 16841 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). 16842 16843 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);` 16844 16845 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 16846 16847 History: 16848 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. 16849 +/ 16850 final class ChangeEvent(T) : ChangeEventBase { 16851 this(Widget target, T delegate() getNewValue) { 16852 assert(getNewValue !is null); 16853 this.getNewValue = getNewValue; 16854 super(target); 16855 } 16856 16857 private T delegate() getNewValue; 16858 16859 /++ 16860 Gets the new value that just changed. 16861 +/ 16862 @property T value() { 16863 return getNewValue(); 16864 } 16865 16866 /// compatibility method for old generic Events 16867 static if(is(immutable T == immutable int)) 16868 override int intValue() { return value; } 16869 /// ditto 16870 static if(is(immutable T == immutable string)) 16871 override string stringValue() { return value; } 16872 } 16873 16874 /++ 16875 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 16876 16877 16878 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16879 16880 History: 16881 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16882 +/ 16883 abstract class KeyEventBase : Event { 16884 this(string name, Widget target) { 16885 super(name, target); 16886 } 16887 16888 // for key events 16889 Key key; /// 16890 16891 KeyEvent originalKeyEvent; 16892 16893 /++ 16894 Indicates the current state of the given keyboard modifier keys. 16895 16896 History: 16897 Added to events on April 15, 2020. 16898 +/ 16899 bool ctrlKey; 16900 16901 /// ditto 16902 bool altKey; 16903 16904 /// ditto 16905 bool shiftKey; 16906 16907 /++ 16908 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 16909 16910 See [arsd.simpledisplay.ModifierState] for other possible flags. 16911 +/ 16912 int state; 16913 16914 mixin Register; 16915 } 16916 16917 /++ 16918 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]. 16919 16920 16921 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16922 16923 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. 16924 16925 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. 16926 16927 See_Also: [KeyUpEvent], [CharEvent] 16928 16929 History: 16930 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 16931 +/ 16932 final class KeyDownEvent : KeyEventBase { 16933 enum EventString = "keydown"; 16934 this(Widget target) { super(EventString, target); } 16935 } 16936 16937 /++ 16938 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 16939 16940 16941 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16942 16943 See_Also: [KeyDownEvent], [CharEvent] 16944 16945 History: 16946 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 16947 +/ 16948 final class KeyUpEvent : KeyEventBase { 16949 enum EventString = "keyup"; 16950 this(Widget target) { super(EventString, target); } 16951 } 16952 16953 /++ 16954 Contains shared properties for various mouse events; 16955 16956 16957 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16958 16959 History: 16960 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16961 +/ 16962 abstract class MouseEventBase : Event { 16963 this(string name, Widget target) { 16964 super(name, target); 16965 } 16966 16967 // for mouse events 16968 int clientX; /// The mouse event location relative to the target widget 16969 int clientY; /// ditto 16970 16971 int viewportX; /// The mouse event location relative to the window origin 16972 int viewportY; /// ditto 16973 16974 int button; /// See: [MouseEvent.button] 16975 int buttonLinear; /// See: [MouseEvent.buttonLinear] 16976 16977 /++ 16978 Indicates the current state of the given keyboard modifier keys. 16979 16980 History: 16981 Added to mouse events on September 28, 2010. 16982 +/ 16983 bool ctrlKey; 16984 16985 /// ditto 16986 bool altKey; 16987 16988 /// ditto 16989 bool shiftKey; 16990 16991 16992 16993 int state; /// 16994 16995 /++ 16996 for consistent names with key event. 16997 16998 History: 16999 Added September 28, 2021 (dub v10.3) 17000 +/ 17001 alias modifierState = state; 17002 17003 /++ 17004 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 17005 17006 History: 17007 Added May 15, 2021 17008 +/ 17009 bool isMouseWheel() { 17010 return button == MouseButton.wheelUp || button == MouseButton.wheelDown || button == MouseButton.wheelLeft || button == MouseButton.wheelRight; 17011 } 17012 17013 // private 17014 override void adjustClientCoordinates(int deltaX, int deltaY) { 17015 clientX += deltaX; 17016 clientY += deltaY; 17017 } 17018 17019 mixin Register; 17020 } 17021 17022 /++ 17023 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 17024 17025 17026 $(WARNING 17027 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 17028 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 17029 behavior. 17030 17031 Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others. 17032 ) 17033 17034 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 17035 17036 [MouseUpEvent] is sent when the user releases a mouse button. 17037 17038 [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.) 17039 17040 [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. 17041 17042 [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. 17043 17044 [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. 17045 17046 [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. 17047 17048 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 17049 17050 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 17051 17052 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 17053 17054 Rationale: 17055 17056 If you only want to do drag, mousedown/up works just fine being consistently sent. 17057 17058 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). 17059 17060 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. 17061 17062 History: 17063 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. 17064 +/ 17065 final class MouseUpEvent : MouseEventBase { 17066 enum EventString = "mouseup"; /// 17067 this(Widget target) { super(EventString, target); } 17068 } 17069 /// ditto 17070 final class MouseDownEvent : MouseEventBase { 17071 enum EventString = "mousedown"; /// 17072 this(Widget target) { super(EventString, target); } 17073 } 17074 /// ditto 17075 final class MouseMoveEvent : MouseEventBase { 17076 enum EventString = "mousemove"; /// 17077 this(Widget target) { super(EventString, target); } 17078 } 17079 /// ditto 17080 final class ClickEvent : MouseEventBase { 17081 enum EventString = "click"; /// 17082 this(Widget target) { super(EventString, target); } 17083 } 17084 /// ditto 17085 final class DoubleClickEvent : MouseEventBase { 17086 enum EventString = "dblclick"; /// 17087 this(Widget target) { super(EventString, target); } 17088 } 17089 /// ditto 17090 final class MouseOverEvent : Event { 17091 enum EventString = "mouseover"; /// 17092 this(Widget target) { super(EventString, target); } 17093 } 17094 /// ditto 17095 final class MouseOutEvent : Event { 17096 enum EventString = "mouseout"; /// 17097 this(Widget target) { super(EventString, target); } 17098 } 17099 /// ditto 17100 final class MouseEnterEvent : Event { 17101 enum EventString = "mouseenter"; /// 17102 this(Widget target) { super(EventString, target); } 17103 17104 override bool propagates() const { return false; } 17105 } 17106 /// ditto 17107 final class MouseLeaveEvent : Event { 17108 enum EventString = "mouseleave"; /// 17109 this(Widget target) { super(EventString, target); } 17110 17111 override bool propagates() const { return false; } 17112 } 17113 17114 private bool isAParentOf(Widget a, Widget b) { 17115 if(a is null || b is null) 17116 return false; 17117 17118 while(b !is null) { 17119 if(a is b) 17120 return true; 17121 b = b.parent; 17122 } 17123 17124 return false; 17125 } 17126 17127 private struct WidgetAtPointResponse { 17128 Widget widget; 17129 17130 // x, y relative to the widget in the response. 17131 int x; 17132 int y; 17133 } 17134 17135 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 17136 assert(starting !is null); 17137 17138 starting.addScrollPosition(x, y); 17139 17140 auto child = starting.getChildAtPosition(x, y); 17141 while(child) { 17142 if(child.hidden) 17143 continue; 17144 starting = child; 17145 x -= child.x; 17146 y -= child.y; 17147 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 17148 child = r.widget; 17149 if(child is starting) 17150 break; 17151 } 17152 return WidgetAtPointResponse(starting, x, y); 17153 } 17154 17155 version(win32_widgets) { 17156 private: 17157 import core.sys.windows.commctrl; 17158 17159 pragma(lib, "comctl32"); 17160 shared static this() { 17161 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 17162 INITCOMMONCONTROLSEX ic; 17163 ic.dwSize = cast(DWORD) ic.sizeof; 17164 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 17165 if(!InitCommonControlsEx(&ic)) { 17166 //writeln("ICC failed"); 17167 } 17168 } 17169 17170 17171 // everything from here is just win32 headers copy pasta 17172 private: 17173 extern(Windows): 17174 17175 alias HANDLE HMENU; 17176 HMENU CreateMenu(); 17177 bool SetMenu(HWND, HMENU); 17178 HMENU CreatePopupMenu(); 17179 enum MF_POPUP = 0x10; 17180 enum MF_STRING = 0; 17181 17182 17183 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 17184 struct INITCOMMONCONTROLSEX { 17185 DWORD dwSize; 17186 DWORD dwICC; 17187 } 17188 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 17189 enum { 17190 IDB_STD_SMALL_COLOR, 17191 IDB_STD_LARGE_COLOR, 17192 IDB_VIEW_SMALL_COLOR = 4, 17193 IDB_VIEW_LARGE_COLOR = 5 17194 } 17195 enum { 17196 STD_CUT, 17197 STD_COPY, 17198 STD_PASTE, 17199 STD_UNDO, 17200 STD_REDOW, 17201 STD_DELETE, 17202 STD_FILENEW, 17203 STD_FILEOPEN, 17204 STD_FILESAVE, 17205 STD_PRINTPRE, 17206 STD_PROPERTIES, 17207 STD_HELP, 17208 STD_FIND, 17209 STD_REPLACE, 17210 STD_PRINT // = 14 17211 } 17212 17213 alias HANDLE HIMAGELIST; 17214 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 17215 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 17216 BOOL ImageList_Destroy(HIMAGELIST); 17217 17218 uint MAKELONG(ushort a, ushort b) { 17219 return cast(uint) ((b << 16) | a); 17220 } 17221 17222 17223 struct TBBUTTON { 17224 int iBitmap; 17225 int idCommand; 17226 BYTE fsState; 17227 BYTE fsStyle; 17228 version(Win64) 17229 BYTE[6] bReserved; 17230 else 17231 BYTE[2] bReserved; 17232 DWORD dwData; 17233 INT_PTR iString; 17234 } 17235 17236 enum { 17237 TB_ADDBUTTONSA = WM_USER + 20, 17238 TB_INSERTBUTTONA = WM_USER + 21, 17239 TB_GETIDEALSIZE = WM_USER + 99, 17240 } 17241 17242 struct SIZE { 17243 LONG cx; 17244 LONG cy; 17245 } 17246 17247 17248 enum { 17249 TBSTATE_CHECKED = 1, 17250 TBSTATE_PRESSED = 2, 17251 TBSTATE_ENABLED = 4, 17252 TBSTATE_HIDDEN = 8, 17253 TBSTATE_INDETERMINATE = 16, 17254 TBSTATE_WRAP = 32 17255 } 17256 17257 17258 17259 enum { 17260 ILC_COLOR = 0, 17261 ILC_COLOR4 = 4, 17262 ILC_COLOR8 = 8, 17263 ILC_COLOR16 = 16, 17264 ILC_COLOR24 = 24, 17265 ILC_COLOR32 = 32, 17266 ILC_COLORDDB = 254, 17267 ILC_MASK = 1, 17268 ILC_PALETTE = 2048 17269 } 17270 17271 17272 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 17273 17274 17275 enum { 17276 TB_ENABLEBUTTON = WM_USER + 1, 17277 TB_CHECKBUTTON, 17278 TB_PRESSBUTTON, 17279 TB_HIDEBUTTON, 17280 TB_INDETERMINATE, // = WM_USER + 5, 17281 TB_ISBUTTONENABLED = WM_USER + 9, 17282 TB_ISBUTTONCHECKED, 17283 TB_ISBUTTONPRESSED, 17284 TB_ISBUTTONHIDDEN, 17285 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 17286 TB_SETSTATE = WM_USER + 17, 17287 TB_GETSTATE = WM_USER + 18, 17288 TB_ADDBITMAP = WM_USER + 19, 17289 TB_DELETEBUTTON = WM_USER + 22, 17290 TB_GETBUTTON, 17291 TB_BUTTONCOUNT, 17292 TB_COMMANDTOINDEX, 17293 TB_SAVERESTOREA, 17294 TB_CUSTOMIZE, 17295 TB_ADDSTRINGA, 17296 TB_GETITEMRECT, 17297 TB_BUTTONSTRUCTSIZE, 17298 TB_SETBUTTONSIZE, 17299 TB_SETBITMAPSIZE, 17300 TB_AUTOSIZE, // = WM_USER + 33, 17301 TB_GETTOOLTIPS = WM_USER + 35, 17302 TB_SETTOOLTIPS = WM_USER + 36, 17303 TB_SETPARENT = WM_USER + 37, 17304 TB_SETROWS = WM_USER + 39, 17305 TB_GETROWS, 17306 TB_GETBITMAPFLAGS, 17307 TB_SETCMDID, 17308 TB_CHANGEBITMAP, 17309 TB_GETBITMAP, 17310 TB_GETBUTTONTEXTA, 17311 TB_REPLACEBITMAP, // = WM_USER + 46, 17312 TB_GETBUTTONSIZE = WM_USER + 58, 17313 TB_SETBUTTONWIDTH = WM_USER + 59, 17314 TB_GETBUTTONTEXTW = WM_USER + 75, 17315 TB_SAVERESTOREW = WM_USER + 76, 17316 TB_ADDSTRINGW = WM_USER + 77, 17317 } 17318 17319 extern(Windows) 17320 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 17321 17322 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 17323 17324 17325 enum { 17326 TB_SETINDENT = WM_USER + 47, 17327 TB_SETIMAGELIST, 17328 TB_GETIMAGELIST, 17329 TB_LOADIMAGES, 17330 TB_GETRECT, 17331 TB_SETHOTIMAGELIST, 17332 TB_GETHOTIMAGELIST, 17333 TB_SETDISABLEDIMAGELIST, 17334 TB_GETDISABLEDIMAGELIST, 17335 TB_SETSTYLE, 17336 TB_GETSTYLE, 17337 //TB_GETBUTTONSIZE, 17338 //TB_SETBUTTONWIDTH, 17339 TB_SETMAXTEXTROWS, 17340 TB_GETTEXTROWS // = WM_USER + 61 17341 } 17342 17343 enum { 17344 CCM_FIRST = 0x2000, 17345 CCM_LAST = CCM_FIRST + 0x200, 17346 CCM_SETBKCOLOR = 8193, 17347 CCM_SETCOLORSCHEME = 8194, 17348 CCM_GETCOLORSCHEME = 8195, 17349 CCM_GETDROPTARGET = 8196, 17350 CCM_SETUNICODEFORMAT = 8197, 17351 CCM_GETUNICODEFORMAT = 8198, 17352 CCM_SETVERSION = 0x2007, 17353 CCM_GETVERSION = 0x2008, 17354 CCM_SETNOTIFYWINDOW = 0x2009 17355 } 17356 17357 17358 enum { 17359 PBM_SETRANGE = WM_USER + 1, 17360 PBM_SETPOS, 17361 PBM_DELTAPOS, 17362 PBM_SETSTEP, 17363 PBM_STEPIT, // = WM_USER + 5 17364 PBM_SETRANGE32 = 1030, 17365 PBM_GETRANGE, 17366 PBM_GETPOS, 17367 PBM_SETBARCOLOR, // = 1033 17368 PBM_SETBKCOLOR = CCM_SETBKCOLOR 17369 } 17370 17371 enum { 17372 PBS_SMOOTH = 1, 17373 PBS_VERTICAL = 4 17374 } 17375 17376 enum { 17377 ICC_LISTVIEW_CLASSES = 1, 17378 ICC_TREEVIEW_CLASSES = 2, 17379 ICC_BAR_CLASSES = 4, 17380 ICC_TAB_CLASSES = 8, 17381 ICC_UPDOWN_CLASS = 16, 17382 ICC_PROGRESS_CLASS = 32, 17383 ICC_HOTKEY_CLASS = 64, 17384 ICC_ANIMATE_CLASS = 128, 17385 ICC_WIN95_CLASSES = 255, 17386 ICC_DATE_CLASSES = 256, 17387 ICC_USEREX_CLASSES = 512, 17388 ICC_COOL_CLASSES = 1024, 17389 ICC_STANDARD_CLASSES = 0x00004000, 17390 } 17391 17392 enum WM_USER = 1024; 17393 } 17394 17395 version(win32_widgets) 17396 pragma(lib, "comdlg32"); 17397 17398 17399 /// 17400 enum GenericIcons : ushort { 17401 None, /// 17402 // these happen to match the win32 std icons numerically if you just subtract one from the value 17403 Cut, /// 17404 Copy, /// 17405 Paste, /// 17406 Undo, /// 17407 Redo, /// 17408 Delete, /// 17409 New, /// 17410 Open, /// 17411 Save, /// 17412 PrintPreview, /// 17413 Properties, /// 17414 Help, /// 17415 Find, /// 17416 Replace, /// 17417 Print, /// 17418 } 17419 17420 enum FileDialogType { 17421 Automatic, 17422 Open, 17423 Save 17424 } 17425 17426 /++ 17427 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. 17428 +/ 17429 string previousFileReferenced; 17430 17431 /++ 17432 Used in automatic menu functions to indicate that the user should be able to browse for a file. 17433 17434 Params: 17435 storage = an alias to a `static string` variable that stores the last file referenced. It will 17436 use this to pre-fill the dialog with a suggestion. 17437 17438 Please note that it MUST be `static` or you will get compile errors. 17439 17440 filters = the filters param to [getFileName] 17441 17442 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 17443 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 17444 a save dialog box. Otherwise, it will show an open dialog box. 17445 +/ 17446 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 17447 string name; 17448 alias name this; 17449 17450 @implicit this(string name) { 17451 this.name = name; 17452 } 17453 } 17454 17455 /++ 17456 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. 17457 17458 History: 17459 onCancel was added November 6, 2021. 17460 17461 The dialog itself on Linux was modified on December 2, 2021 to include 17462 a directory picker in addition to the command line completion view. 17463 17464 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 17465 17466 The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version. 17467 Future_directions: 17468 I want to add some kind of custom preview and maybe thumbnail thing in the future, 17469 at least on Linux, maybe on Windows too. 17470 +/ 17471 void getOpenFileName( 17472 Window owner, 17473 void delegate(string) onOK, 17474 string prefilledName = null, 17475 string[] filters = null, 17476 void delegate() onCancel = null, 17477 string initialDirectory = null, 17478 ) 17479 { 17480 return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory); 17481 } 17482 17483 /// ditto 17484 void getSaveFileName( 17485 Window owner, 17486 void delegate(string) onOK, 17487 string prefilledName = null, 17488 string[] filters = null, 17489 void delegate() onCancel = null, 17490 string initialDirectory = null, 17491 ) 17492 { 17493 return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory); 17494 } 17495 17496 /// ditto 17497 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.") 17498 void getOpenFileName( 17499 void delegate(string) onOK, 17500 string prefilledName = null, 17501 string[] filters = null, 17502 void delegate() onCancel = null, 17503 string initialDirectory = null, 17504 ) 17505 { 17506 return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory); 17507 } 17508 17509 /// ditto 17510 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.") 17511 void getSaveFileName( 17512 void delegate(string) onOK, 17513 string prefilledName = null, 17514 string[] filters = null, 17515 void delegate() onCancel = null, 17516 string initialDirectory = null, 17517 ) 17518 { 17519 return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory); 17520 } 17521 17522 /++ 17523 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. 17524 17525 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. 17526 17527 History: 17528 Added January 1, 2025 17529 +/ 17530 class FileDialogDelegate { 17531 17532 /++ 17533 17534 +/ 17535 static abstract class PreviewWidget : Widget { 17536 /// Call this from your subclass' constructor 17537 this(Widget parent) { 17538 super(parent); 17539 } 17540 17541 /// Load the file given to you and show its preview inside the widget here 17542 abstract void previewFile(string filename); 17543 } 17544 17545 /++ 17546 Override this to add preview capabilities to the dialog for certain files. 17547 +/ 17548 protected PreviewWidget makePreviewWidget(Widget parent) { 17549 return null; 17550 } 17551 17552 /++ 17553 Override this to change the dialog entirely. 17554 17555 This function IS allowed to block, but is NOT required to. 17556 +/ 17557 protected void getFileName( 17558 Window owner, 17559 bool openOrSave, // true if open, false if save 17560 void delegate(string) onOK, 17561 string prefilledName, 17562 string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17563 void delegate() onCancel, 17564 string initialDirectory, 17565 ) 17566 { 17567 17568 version(win32_widgets) { 17569 import core.sys.windows.commdlg; 17570 /* 17571 Ofn.lStructSize = sizeof(OPENFILENAME); 17572 Ofn.hwndOwner = hWnd; 17573 Ofn.lpstrFilter = szFilter; 17574 Ofn.lpstrFile= szFile; 17575 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 17576 Ofn.lpstrFileTitle = szFileTitle; 17577 Ofn.nMaxFileTitle = sizeof(szFileTitle); 17578 Ofn.lpstrInitialDir = (LPSTR)NULL; 17579 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 17580 Ofn.lpstrTitle = szTitle; 17581 */ 17582 17583 17584 wchar[1024] file = 0; 17585 wchar[1024] filterBuffer = 0; 17586 makeWindowsString(prefilledName, file[]); 17587 OPENFILENAME ofn; 17588 ofn.lStructSize = ofn.sizeof; 17589 ofn.hwndOwner = owner is null ? null : owner.win.hwnd; 17590 if(filters.length) { 17591 string filter; 17592 foreach(i, f; filters) { 17593 filter ~= f; 17594 filter ~= "\0"; 17595 } 17596 filter ~= "\0"; 17597 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 17598 } 17599 ofn.lpstrFile = file.ptr; 17600 ofn.nMaxFile = file.length; 17601 17602 wchar[1024] initialDir = 0; 17603 if(initialDirectory !is null) { 17604 makeWindowsString(initialDirectory, initialDir[]); 17605 ofn.lpstrInitialDir = file.ptr; 17606 } 17607 17608 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 17609 { 17610 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 17611 if(okString.length && okString[$-1] == '\0') 17612 okString = okString[0..$-1]; 17613 onOK(okString); 17614 } else { 17615 if(onCancel) 17616 onCancel(); 17617 } 17618 } else version(custom_widgets) { 17619 filters ~= ["All Files\0*.*"]; 17620 auto picker = new FilePicker(false, openOrSave, prefilledName, filters, initialDirectory, owner); 17621 picker.onOK = onOK; 17622 picker.onCancel = onCancel; 17623 picker.show(); 17624 } 17625 } 17626 } 17627 17628 /// ditto 17629 FileDialogDelegate fileDialogDelegate() { 17630 if(fileDialogDelegate_ is null) 17631 fileDialogDelegate_ = new FileDialogDelegate(); 17632 return fileDialogDelegate_; 17633 } 17634 17635 /// ditto 17636 void fileDialogDelegate(FileDialogDelegate replacement) { 17637 fileDialogDelegate_ = replacement; 17638 } 17639 17640 private FileDialogDelegate fileDialogDelegate_; 17641 17642 struct FileNameFilter { 17643 string description; 17644 string[] globPatterns; 17645 17646 string toString() { 17647 string ret; 17648 ret ~= description; 17649 ret ~= " ("; 17650 foreach(idx, pattern; globPatterns) { 17651 if(idx) 17652 ret ~= "; "; 17653 ret ~= pattern; 17654 } 17655 ret ~= ")"; 17656 17657 return ret; 17658 } 17659 17660 static FileNameFilter fromString(string s) { 17661 size_t end = s.length; 17662 size_t start = 0; 17663 foreach_reverse(idx, ch; s) { 17664 if(ch == ')' && end == s.length) 17665 end = idx; 17666 else if(ch == '(' && end != s.length) { 17667 start = idx + 1; 17668 break; 17669 } 17670 } 17671 17672 FileNameFilter fnf; 17673 fnf.description = s[0 .. start ? start - 1 : 0]; 17674 size_t globStart = 0; 17675 s = s[start .. end]; 17676 foreach(idx, ch; s) 17677 if(ch == ';') { 17678 auto ptn = stripInternal(s[globStart .. idx]); 17679 if(ptn.length) 17680 fnf.globPatterns ~= ptn; 17681 globStart = idx + 1; 17682 17683 } 17684 auto ptn = stripInternal(s[globStart .. $]); 17685 if(ptn.length) 17686 fnf.globPatterns ~= ptn; 17687 return fnf; 17688 } 17689 } 17690 17691 struct FileNameFilterSet { 17692 FileNameFilter[] filters; 17693 17694 static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) { 17695 FileNameFilter[] ret; 17696 17697 foreach(filter; filters) { 17698 FileNameFilter fnf; 17699 size_t filterStartPoint; 17700 foreach(idx, ch; filter) { 17701 if(ch == 0) { 17702 fnf.description = filter[0 .. idx]; 17703 filterStartPoint = idx + 1; 17704 } else if(filterStartPoint && ch == ';') { 17705 fnf.globPatterns ~= filter[filterStartPoint .. idx]; 17706 filterStartPoint = idx + 1; 17707 } 17708 } 17709 fnf.globPatterns ~= filter[filterStartPoint .. $]; 17710 17711 ret ~= fnf; 17712 } 17713 17714 return FileNameFilterSet(ret); 17715 } 17716 } 17717 17718 void getFileName( 17719 Window owner, 17720 bool openOrSave, 17721 void delegate(string) onOK, 17722 string prefilledName = null, 17723 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17724 void delegate() onCancel = null, 17725 string initialDirectory = null, 17726 ) 17727 { 17728 return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory); 17729 } 17730 17731 version(custom_widgets) 17732 private 17733 class FilePicker : Dialog { 17734 void delegate(string) onOK; 17735 void delegate() onCancel; 17736 LabeledLineEdit lineEdit; 17737 bool isOpenDialogInsteadOfSave; 17738 bool requireExistingFile; 17739 17740 static struct HistoryItem { 17741 string cwd; 17742 FileNameFilter filters; 17743 } 17744 HistoryItem[] historyStack; 17745 size_t historyStackPosition; 17746 17747 void back() { 17748 if(historyStackPosition) { 17749 historyStackPosition--; 17750 currentDirectory = historyStack[historyStackPosition].cwd; 17751 currentFilter = historyStack[historyStackPosition].filters; 17752 filesOfType.content = currentFilter.toString(); 17753 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 17754 lineEdit.focus(); 17755 } 17756 } 17757 17758 void forward() { 17759 if(historyStackPosition + 1 < historyStack.length) { 17760 historyStackPosition++; 17761 currentDirectory = historyStack[historyStackPosition].cwd; 17762 currentFilter = historyStack[historyStackPosition].filters; 17763 filesOfType.content = currentFilter.toString(); 17764 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 17765 lineEdit.focus(); 17766 } 17767 } 17768 17769 void up() { 17770 currentDirectory = currentDirectory ~ ".."; 17771 loadFiles(currentDirectory, currentFilter); 17772 lineEdit.focus(); 17773 } 17774 17775 void refresh() { 17776 loadFiles(currentDirectory, currentFilter); 17777 lineEdit.focus(); 17778 } 17779 17780 // returns common prefix 17781 static struct CommonPrefixInfo { 17782 string commonPrefix; 17783 int fileCount; 17784 string exactMatch; 17785 } 17786 CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) { 17787 17788 if(!comingFromHistory) { 17789 if(historyStack.length) { 17790 historyStack = historyStack[0 .. historyStackPosition + 1]; 17791 historyStack.assumeSafeAppend(); 17792 } 17793 historyStack ~= HistoryItem(cwd, filters); 17794 historyStackPosition = historyStack.length - 1; 17795 } 17796 17797 string[] files; 17798 string[] dirs; 17799 17800 dirs ~= "$HOME"; 17801 dirs ~= "$PWD"; 17802 17803 string commonPrefix; 17804 int commonPrefixCount; 17805 string exactMatch; 17806 17807 bool matchesFilter(string name) { 17808 foreach(filter; filters.globPatterns) { 17809 if( 17810 filter.length <= 1 || 17811 filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .* 17812 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 17813 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 17814 ) 17815 { 17816 if(name.length > 1 && name[0] == '.') 17817 if(filter.length == 0 || filter[0] != '.') 17818 return false; 17819 17820 return true; 17821 } 17822 } 17823 17824 return false; 17825 } 17826 17827 void considerCommonPrefix(string name, bool prefiltered) { 17828 if(!prefiltered && !matchesFilter(name)) 17829 return; 17830 17831 if(commonPrefix is null) { 17832 commonPrefix = name; 17833 commonPrefixCount = 1; 17834 exactMatch = commonPrefix; 17835 } else { 17836 foreach(idx, char i; name) { 17837 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 17838 commonPrefix = commonPrefix[0 .. idx]; 17839 commonPrefixCount ++; 17840 exactMatch = null; 17841 break; 17842 } 17843 } 17844 } 17845 } 17846 17847 bool applyFilterToDirectories = true; 17848 bool showDotFiles = false; 17849 foreach(filter; filters.globPatterns) { 17850 if(filter == ".*") 17851 showDotFiles = true; 17852 else foreach(ch; filter) 17853 if(ch == '.') { 17854 // a filter like *.exe should not apply to the directory 17855 applyFilterToDirectories = false; 17856 break; 17857 } 17858 } 17859 17860 try 17861 getFiles(cwd, (string name, bool isDirectory) { 17862 if(name == ".") 17863 return; // skip this as unnecessary 17864 if(isDirectory) { 17865 if(applyFilterToDirectories) { 17866 if(matchesFilter(name)) { 17867 dirs ~= name; 17868 considerCommonPrefix(name, false); 17869 } 17870 } else if(name != ".." && name.length > 1 && name[0] == '.') { 17871 if(showDotFiles) { 17872 dirs ~= name; 17873 considerCommonPrefix(name, false); 17874 } 17875 } else { 17876 dirs ~= name; 17877 considerCommonPrefix(name, false); 17878 } 17879 } else { 17880 if(matchesFilter(name)) { 17881 files ~= name; 17882 17883 //if(filter.length > 0 && filter[$-1] == '*') { 17884 considerCommonPrefix(name, true); 17885 //} 17886 } 17887 } 17888 }); 17889 catch(ArsdExceptionBase e) { 17890 messageBox("Unable to read requested directory"); 17891 // FIXME: give them a chance to create it? or at least go back? 17892 /+ 17893 comingFromHistory = true; 17894 back(); 17895 return null; 17896 +/ 17897 } 17898 17899 extern(C) static int comparator(scope const void* a, scope const void* b) { 17900 auto sa = *cast(string*) a; 17901 auto sb = *cast(string*) b; 17902 17903 /+ 17904 Goal here: 17905 17906 Dot first. This puts `foo.d` before `foo2.d` 17907 Then numbers , natural sort order (so 9 comes before 10) for positive numbers 17908 Then letters, in order Aa, Bb, Cc 17909 Then other symbols in ascii order 17910 +/ 17911 static int nextPiece(ref string whole) { 17912 if(whole.length == 0) 17913 return -1; 17914 17915 enum specialZoneSize = 1; 17916 string originalString = whole; 17917 bool fallback; 17918 17919 start_over: 17920 17921 char current = whole[0]; 17922 if(!fallback && current >= '0' && current <= '9') { 17923 // if this overflows, it can mess up the sort, so it will fallback to string sort in that event 17924 int accumulator; 17925 do { 17926 auto before = accumulator; 17927 whole = whole[1 .. $]; 17928 accumulator *= 10; 17929 accumulator += current - '0'; 17930 current = whole.length ? whole[0] : 0; 17931 if(accumulator < before) { 17932 fallback = true; 17933 whole = originalString; 17934 goto start_over; 17935 } 17936 } while (current >= '0' && current <= '9'); 17937 17938 return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols 17939 } else { 17940 whole = whole[1 .. $]; 17941 17942 if(current == '.') 17943 return 0; // the special case to put it before numbers 17944 17945 // anything above should be < specialZoneSize 17946 17947 int letterZoneSize = 26 * 2; 17948 int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars 17949 17950 if(current >= 'A' && current <= 'Z') 17951 return base + (current - 'A') * 2; 17952 if(current >= 'a' && current <= 'z') 17953 return base + (current - 'a') * 2 + 1; 17954 // return base + letterZoneSize + current; // would put symbols after numbers and letters 17955 return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone 17956 } 17957 } 17958 17959 while(sa.length || sb.length) { 17960 auto pa = nextPiece(sa); 17961 auto pb = nextPiece(sb); 17962 17963 auto diff = pa - pb; 17964 if(diff) 17965 return diff; 17966 } 17967 17968 return 0; 17969 } 17970 17971 nonPhobosSort(files, &comparator); 17972 nonPhobosSort(dirs, &comparator); 17973 17974 listWidget.clear(); 17975 dirWidget.clear(); 17976 foreach(name; dirs) 17977 dirWidget.addOption(name); 17978 foreach(name; files) 17979 listWidget.addOption(name); 17980 17981 return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch); 17982 } 17983 17984 ListWidget listWidget; 17985 ListWidget dirWidget; 17986 17987 FreeEntrySelection filesOfType; 17988 LineEdit directoryHolder; 17989 17990 string currentDirectory_; 17991 FileNameFilter currentNonTabFilter; 17992 FileNameFilter currentFilter; 17993 FileNameFilterSet filterOptions; 17994 17995 void currentDirectory(string s) { 17996 currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString(); 17997 directoryHolder.content = currentDirectory_; 17998 } 17999 string currentDirectory() { 18000 return currentDirectory_; 18001 } 18002 18003 private string getUserHomeDir() { 18004 import core.stdc.stdlib; 18005 version(Windows) 18006 return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup; 18007 else 18008 return (stringz(getenv("HOME")).borrow).idup; 18009 } 18010 18011 private string expandTilde(string s) { 18012 // FIXME: cannot look up other user dirs 18013 if(s.length == 1 && s == "~") 18014 return getUserHomeDir(); 18015 if(s.length > 1 && s[0] == '~' && s[1] == '/') 18016 return getUserHomeDir() ~ s[1 .. $]; 18017 return s; 18018 } 18019 18020 // FIXME: allow many files to be picked too sometimes 18021 18022 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 18023 this(bool requireExistingFile, bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) { 18024 this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat); 18025 this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave; 18026 this.requireExistingFile = requireExistingFile; 18027 super(owner, 500, 400, "Choose File..."); // owner); 18028 18029 { 18030 auto navbar = new HorizontalLayout(24, this); 18031 auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar); 18032 auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar); 18033 auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button 18034 18035 directoryHolder = new LineEdit(navbar); 18036 18037 directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) { 18038 if(kde.key == Key.Enter || kde.key == Key.PadEnter) { 18039 kde.stopPropagation(); 18040 18041 currentDirectory = directoryHolder.content; 18042 loadFiles(currentDirectory, currentFilter); 18043 18044 lineEdit.focus(); 18045 } 18046 }); 18047 18048 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. 18049 18050 /+ 18051 auto newDirectoryButton = new ToolButton(new Action("N"), navbar); 18052 18053 // FIXME: make sure putting `.` in the dir filter goes back to the CWD 18054 // and that ~ goes back to the home dir 18055 // and blanking it goes back to the suggested dir 18056 18057 auto homeButton = new ToolButton(new Action("H"), navbar); 18058 auto cwdButton = new ToolButton(new Action("."), navbar); 18059 auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar); 18060 +/ 18061 18062 filesOfType = new class FreeEntrySelection { 18063 this() { 18064 string[] opt; 18065 foreach(option; filterOptions.filters) 18066 opt ~= option.toString; 18067 super(opt, navbar); 18068 } 18069 override int flexBasisWidth() { 18070 return scaleWithDpi(150); 18071 } 18072 override int widthStretchiness() { 18073 return 1;//super.widthStretchiness() / 2; 18074 } 18075 }; 18076 filesOfType.setSelection(0); 18077 currentFilter = filterOptions.filters[0]; 18078 currentNonTabFilter = currentFilter; 18079 } 18080 18081 { 18082 auto mainGrid = new GridLayout(4, 1, this); 18083 18084 dirWidget = new ListWidget(mainGrid); 18085 listWidget = new ListWidget(mainGrid); 18086 listWidget.tabStop = false; 18087 dirWidget.tabStop = false; 18088 18089 FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid); 18090 18091 mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1); 18092 mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1); 18093 if(previewWidget) 18094 mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1); 18095 18096 // double click events normally trigger something else but 18097 // here user might be clicking kinda fast and we'd rather just 18098 // keep it 18099 dirWidget.addEventListener((scope DoubleClickEvent dev) { 18100 auto ce = new ChangeEvent!void(dirWidget, () {}); 18101 ce.dispatch(); 18102 lineEdit.focus(); 18103 }); 18104 18105 dirWidget.addEventListener((scope ChangeEvent!void sce) { 18106 string v; 18107 foreach(o; dirWidget.options) 18108 if(o.selected) { 18109 v = o.label; 18110 break; 18111 } 18112 if(v.length) { 18113 if(v == "$HOME") 18114 currentDirectory = getUserHomeDir(); 18115 else if(v == "$PWD") 18116 currentDirectory = "."; 18117 else 18118 currentDirectory = currentDirectory ~ "/" ~ v; 18119 loadFiles(currentDirectory, currentFilter); 18120 } 18121 18122 dirWidget.focusOn = -1; 18123 lineEdit.focus(); 18124 }); 18125 18126 // double click here, on the other hand, selects the file 18127 // and moves on 18128 listWidget.addEventListener((scope DoubleClickEvent dev) { 18129 OK(); 18130 }); 18131 } 18132 18133 lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this); 18134 lineEdit.focus(); 18135 lineEdit.addEventListener(delegate(CharEvent event) { 18136 if(event.character == '\t' || event.character == '\n') 18137 event.preventDefault(); 18138 }); 18139 18140 listWidget.addEventListener(EventType.change, () { 18141 foreach(o; listWidget.options) 18142 if(o.selected) 18143 lineEdit.content = o.label; 18144 }); 18145 18146 currentDirectory = initialDirectory is null ? "." : initialDirectory; 18147 18148 auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory)); 18149 currentDirectory = prefilledPath.directoryName; 18150 prefilledName = prefilledPath.filename; 18151 loadFiles(currentDirectory, currentFilter); 18152 18153 filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) { 18154 currentFilter = FileNameFilter.fromString(ce.stringValue); 18155 currentNonTabFilter = currentFilter; 18156 loadFiles(currentDirectory, currentFilter); 18157 // lineEdit.focus(); // this causes a recursive crash..... 18158 }); 18159 18160 filesOfType.addEventListener(delegate(KeyDownEvent event) { 18161 if(event.key == Key.Enter) { 18162 currentFilter = FileNameFilter.fromString(filesOfType.content); 18163 currentNonTabFilter = currentFilter; 18164 loadFiles(currentDirectory, currentFilter); 18165 event.stopPropagation(); 18166 // FIXME: refocus on the line edit 18167 } 18168 }); 18169 18170 lineEdit.addEventListener((KeyDownEvent event) { 18171 if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) { 18172 18173 auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory)); 18174 currentDirectory = path.directoryName; 18175 auto current = path.filename; 18176 18177 auto newFilter = current; 18178 if(current.length && current[0] != '*' && current[$-1] != '*') 18179 newFilter ~= "*"; 18180 else if(newFilter.length == 0) 18181 newFilter = "*"; 18182 18183 auto newFilterObj = FileNameFilter("Custom filter", [newFilter]); 18184 18185 CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj); 18186 if(commonPrefix.fileCount == 1) { 18187 // exactly one file, let's see what it is 18188 auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory)); 18189 if(getFileType(specificFile.toString) == FileType.dir) { 18190 // a directory means we should change to it and keep the old filter 18191 currentDirectory = specificFile.toString(); 18192 lineEdit.content = specificFile.toString() ~ "/"; 18193 loadFiles(currentDirectory, currentFilter); 18194 } else { 18195 // any other file should be selected in the list 18196 currentDirectory = specificFile.directoryName; 18197 current = specificFile.filename; 18198 lineEdit.content = current; 18199 loadFiles(currentDirectory, currentFilter); 18200 } 18201 } else if(commonPrefix.fileCount > 1) { 18202 currentFilter = newFilterObj; 18203 filesOfType.content = currentFilter.toString(); 18204 lineEdit.content = commonPrefix.commonPrefix; 18205 } else { 18206 // if there were no files, we don't really want to change the filter.. 18207 //sdpyPrintDebugString("no files"); 18208 } 18209 18210 // FIXME: if that is a directory, add the slash? or even go inside? 18211 18212 event.preventDefault(); 18213 } 18214 else if(event.key == Key.Left && event.altKey) { 18215 this.back(); 18216 event.preventDefault(); 18217 } 18218 else if(event.key == Key.Right && event.altKey) { 18219 this.forward(); 18220 event.preventDefault(); 18221 } 18222 }); 18223 18224 18225 lineEdit.content = prefilledName; 18226 18227 auto hl = new HorizontalLayout(60, this); 18228 auto cancelButton = new Button("Cancel", hl); 18229 auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl); 18230 18231 cancelButton.addEventListener(EventType.triggered, &Cancel); 18232 okButton.addEventListener(EventType.triggered, &OK); 18233 18234 this.addEventListener((KeyDownEvent event) { 18235 if(event.key == Key.Enter || event.key == Key.PadEnter) { 18236 event.preventDefault(); 18237 OK(); 18238 } 18239 else if(event.key == Key.Escape) 18240 Cancel(); 18241 else if(event.key == Key.F5) 18242 refresh(); 18243 else if(event.key == Key.Up && event.altKey) 18244 up(); // ditto 18245 else if(event.key == Key.Left && event.altKey) 18246 back(); // FIXME: it sends the key to the line edit too 18247 else if(event.key == Key.Right && event.altKey) 18248 forward(); // ditto 18249 else if(event.key == Key.Up) 18250 listWidget.setSelection(listWidget.getSelection() - 1); 18251 else if(event.key == Key.Down) 18252 listWidget.setSelection(listWidget.getSelection() + 1); 18253 }); 18254 18255 // FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted 18256 // FIXME: button to create new directory 18257 // FIXME: show dirs in the files list too? idk. 18258 18259 // FIXME: support ~ as alias for home in the input 18260 // FIXME: tab complete ought to be able to change+complete dir too 18261 } 18262 18263 override void OK() { 18264 if(lineEdit.content.length) { 18265 auto c = expandTilde(lineEdit.content); 18266 18267 FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory)); 18268 18269 auto ft = getFileType(accepted.toString); 18270 18271 if(ft == FileType.error && requireExistingFile) { 18272 // FIXME: tell the user why 18273 messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel."); 18274 lineEdit.focus(); 18275 return; 18276 18277 } 18278 18279 // FIXME: symlinks to dirs should prolly also get this behavior 18280 if(ft == FileType.dir) { 18281 currentDirectory = accepted.toString; 18282 18283 currentFilter = currentNonTabFilter; 18284 filesOfType.content = currentFilter.toString(); 18285 18286 loadFiles(currentDirectory, currentFilter); 18287 lineEdit.content = ""; 18288 18289 lineEdit.focus(); 18290 18291 return; 18292 } 18293 18294 if(onOK) 18295 onOK(accepted.toString); 18296 } 18297 close(); 18298 } 18299 18300 override void Cancel() { 18301 if(onCancel) 18302 onCancel(); 18303 close(); 18304 } 18305 } 18306 18307 private enum FileType { 18308 error, 18309 dir, 18310 other 18311 } 18312 18313 private FileType getFileType(string name) { 18314 version(Windows) { 18315 auto ws = WCharzBuffer(name); 18316 auto ret = GetFileAttributesW(ws.ptr); 18317 if(ret == INVALID_FILE_ATTRIBUTES) 18318 return FileType.error; 18319 return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other; 18320 } else version(Posix) { 18321 import core.sys.posix.sys.stat; 18322 stat_t buf; 18323 auto ret = stat((name ~ '\0').ptr, &buf); 18324 if(ret == -1) 18325 return FileType.error; 18326 // FIXME: what about a symlink to a dir? S_IFLNK then readlink then i believe stat it again. 18327 return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other; 18328 } else assert(0, "Not implemented"); 18329 } 18330 18331 /* 18332 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 18333 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 18334 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 18335 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 18336 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 18337 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 18338 http://www.sbin.org/doc/Xlib/chapt_03.html 18339 18340 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 18341 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 18342 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 18343 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 18344 */ 18345 18346 18347 // These are all for setMenuAndToolbarFromAnnotatedCode 18348 /// This item in the menu will be preceded by a separator line 18349 /// Group: generating_from_code 18350 struct separator {} 18351 deprecated("It was misspelled, use separator instead") alias seperator = separator; 18352 /// Program-wide keyboard shortcut to trigger the action 18353 /// Group: generating_from_code 18354 struct accelerator { string keyString; } // FIXME: allow multiple aliases here 18355 /// tells which menu the action will be on 18356 /// Group: generating_from_code 18357 struct menu { string name; } 18358 /// Describes which toolbar section the action appears on 18359 /// Group: generating_from_code 18360 struct toolbar { string groupName; } 18361 /// 18362 /// Group: generating_from_code 18363 struct icon { ushort id; } 18364 /// 18365 /// Group: generating_from_code 18366 struct label { string label; } 18367 /// 18368 /// Group: generating_from_code 18369 struct hotkey { dchar ch; } 18370 /// 18371 /// Group: generating_from_code 18372 struct tip { string tip; } 18373 /// 18374 /// Group: generating_from_code 18375 enum context_menu = menu.init; 18376 /++ 18377 // FIXME: the options should have both a label and a value 18378 18379 if label is null, it will try to just stringify value. 18380 18381 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. 18382 +/ 18383 /// Group: generating_from_code 18384 Choices!T choices(T)(T[] options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) { 18385 return Choices!T(() => options, allowCustom, allowReordering, allowDuplicates); 18386 } 18387 /// ditto 18388 Choices!T choices(T)(T[] function() options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) { 18389 return Choices!T(options, allowCustom, allowReordering, allowDuplicates); 18390 } 18391 /// ditto 18392 struct Choices(T) { 18393 /// 18394 T[] function() options; // IMPORTANT: this MUST be function, not delegate, see https://github.com/dlang/dmd/issues/21915 18395 bool allowCustom = false; 18396 /// only relevant if attached to an array 18397 bool allowReordering = true; 18398 /// ditto 18399 bool allowDuplicates = true; 18400 /// makes no sense on a set 18401 bool requireAll = false; 18402 } 18403 18404 18405 /++ 18406 Observes and allows inspection of an object via automatic gui 18407 +/ 18408 /// Group: generating_from_code 18409 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 18410 return new ObjectInspectionWindowImpl!(T)(t); 18411 } 18412 18413 class ObjectInspectionWindow : Window { 18414 this(int a, int b, string c) { 18415 super(a, b, c); 18416 } 18417 18418 abstract void readUpdatesFromObject(); 18419 } 18420 18421 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 18422 T t; 18423 this(T t) { 18424 this.t = t; 18425 18426 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 18427 18428 foreach(memberName; __traits(derivedMembers, T)) {{ 18429 alias member = I!(__traits(getMember, t, memberName))[0]; 18430 alias type = typeof(member); 18431 static if(is(type == int)) { 18432 auto le = new LabeledLineEdit(memberName ~ ": ", this); 18433 //le.addEventListener("char", (Event ev) { 18434 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 18435 //ev.preventDefault(); 18436 //}); 18437 le.addEventListener(EventType.change, (Event ev) { 18438 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 18439 }); 18440 18441 updateMemberDelegates[memberName] = () { 18442 le.content = toInternal!string(__traits(getMember, t, memberName)); 18443 }; 18444 } 18445 }} 18446 } 18447 18448 void delegate()[string] updateMemberDelegates; 18449 18450 override void readUpdatesFromObject() { 18451 foreach(k, v; updateMemberDelegates) 18452 v(); 18453 } 18454 } 18455 18456 /++ 18457 Creates a dialog based on a data structure. 18458 18459 --- 18460 dialog(window, (YourStructure value) { 18461 // the user filled in the struct and clicked OK, 18462 // you can check the members now 18463 }); 18464 --- 18465 18466 Params: 18467 initialData = the initial value to show in the dialog. It will not modify this unless 18468 it is a class then it might, no promises. 18469 18470 History: 18471 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 18472 18473 The overloads with `parent` were added September 29, 2024. The ones without it are likely to 18474 be deprecated soon. 18475 +/ 18476 /// Group: generating_from_code 18477 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.") 18478 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18479 dialog(null, T.init, onOK, onCancel, title); 18480 } 18481 /// ditto 18482 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.") 18483 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18484 dialog(null, T.init, onOK, onCancel, title); 18485 } 18486 /// ditto 18487 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18488 dialog(parent, T.init, onOK, onCancel, title); 18489 } 18490 /// ditto 18491 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.") 18492 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18493 dialog(parent, initialData, onOK, onCancel, title); 18494 } 18495 /// ditto 18496 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18497 auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title); 18498 dg.show(); 18499 } 18500 18501 private static template I(T...) { alias I = T; } 18502 18503 18504 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 18505 if(name == "id") 18506 return allLowerCase ? name : "ID"; 18507 18508 char[160] buffer; 18509 int bufferIndex = 0; 18510 bool shouldCap = true; 18511 bool shouldSpace; 18512 bool lastWasCap; 18513 foreach(idx, char ch; name) { 18514 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 18515 18516 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 18517 if(lastWasCap) { 18518 // two caps in a row, don't change. Prolly acronym. 18519 } else { 18520 if(idx) 18521 shouldSpace = true; // new word, add space 18522 } 18523 18524 lastWasCap = true; 18525 } else { 18526 lastWasCap = false; 18527 } 18528 18529 if(shouldSpace) { 18530 buffer[bufferIndex++] = space; 18531 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 18532 shouldSpace = false; 18533 } 18534 if(shouldCap) { 18535 if(ch >= 'a' && ch <= 'z') 18536 ch -= 32; 18537 shouldCap = false; 18538 } 18539 if(allLowerCase && ch >= 'A' && ch <= 'Z') 18540 ch += 32; 18541 buffer[bufferIndex++] = ch; 18542 } 18543 return buffer[0 .. bufferIndex].idup; 18544 } 18545 18546 /++ 18547 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. 18548 +/ 18549 class AutomaticDialog(T) : Dialog { 18550 T t; 18551 18552 void delegate(T) onOK; 18553 void delegate() onCancel; 18554 18555 override int paddingTop() { return defaultLineHeight; } 18556 override int paddingBottom() { return defaultLineHeight; } 18557 override int paddingRight() { return defaultLineHeight; } 18558 override int paddingLeft() { return defaultLineHeight; } 18559 18560 this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 18561 assert(onOK !is null); 18562 18563 t = initialData; 18564 18565 static if(is(T == class)) { 18566 if(t is null) 18567 t = new T(); 18568 } 18569 this.onOK = onOK; 18570 this.onCancel = onCancel; 18571 super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 18572 18573 static if(is(T == class)) 18574 this.addDataControllerWidget(t); 18575 else 18576 this.addDataControllerWidget(&t); 18577 18578 auto hl = new HorizontalLayout(this); 18579 auto stretch = new HorizontalSpacer(hl); // to right align 18580 auto ok = new CommandButton("OK", hl); 18581 auto cancel = new CommandButton("Cancel", hl); 18582 ok.addEventListener(EventType.triggered, &OK); 18583 cancel.addEventListener(EventType.triggered, &Cancel); 18584 18585 this.addEventListener((KeyDownEvent ev) { 18586 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 18587 ok.focus(); 18588 OK(); 18589 ev.preventDefault(); 18590 } 18591 if(ev.key == Key.Escape) { 18592 Cancel(); 18593 ev.preventDefault(); 18594 } 18595 }); 18596 18597 this.addEventListener((scope ClosedEvent ce) { 18598 if(onCancel) 18599 onCancel(); 18600 }); 18601 18602 //this.children[0].focus(); 18603 } 18604 18605 override void OK() { 18606 onOK(t); 18607 close(); 18608 } 18609 18610 override void Cancel() { 18611 if(onCancel) 18612 onCancel(); 18613 close(); 18614 } 18615 } 18616 18617 private template baseClassCount(Class) { 18618 private int helper() { 18619 int count = 0; 18620 static if(is(Class bases == super)) { 18621 foreach(base; bases) 18622 static if(is(base == class)) 18623 count += 1 + baseClassCount!base; 18624 } 18625 return count; 18626 } 18627 18628 enum int baseClassCount = helper(); 18629 } 18630 18631 private long stringToLong(string s) { 18632 long ret; 18633 if(s.length == 0) 18634 return ret; 18635 bool negative = s[0] == '-'; 18636 if(negative) 18637 s = s[1 .. $]; 18638 foreach(ch; s) { 18639 if(ch >= '0' && ch <= '9') { 18640 ret *= 10; 18641 ret += ch - '0'; 18642 } 18643 } 18644 if(negative) 18645 ret = -ret; 18646 return ret; 18647 } 18648 18649 18650 interface ReflectableProperties { 18651 /++ 18652 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 18653 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 18654 json in the current implementation. 18655 18656 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 18657 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 18658 as of the June 2, 2021 release. 18659 18660 History: 18661 Added June 2, 2021. 18662 18663 See_Also: [getPropertyAsString], [setPropertyFromString] 18664 +/ 18665 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 18666 /++ 18667 Requests a property to be delivered to you as a string, through your `sink` delegate. 18668 18669 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 18670 be interpreted as json, otherwise, it is just a plain string. 18671 18672 The sink should always be called exactly once for each call (it is basically a return value, but it might 18673 use a local buffer it maintains instead of allocating a return value). 18674 18675 History: 18676 Added June 2, 2021. 18677 18678 See_Also: [getPropertiesList], [setPropertyFromString] 18679 +/ 18680 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 18681 /++ 18682 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. 18683 18684 History: 18685 Added June 2, 2021. 18686 18687 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 18688 +/ 18689 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 18690 18691 /// [setPropertyFromString] possible return values 18692 enum SetPropertyResult { 18693 success = 0, /// the property has been successfully set to the request value 18694 notPermitted = -1, /// the property exists but it cannot be changed at this time 18695 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 18696 noSuchProperty = -3, /// there is no property by that name 18697 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 18698 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) 18699 } 18700 18701 /++ 18702 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 18703 18704 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 18705 18706 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 18707 rarely need to use these building blocks directly. 18708 +/ 18709 mixin template RegisterSetters() { 18710 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 18711 switch(name) { 18712 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18713 case memberName: 18714 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 18715 if(value != "true" && value != "false") 18716 return SetPropertyResult.wrongFormat; 18717 __traits(getMember, this, memberName) = value == "true" ? true : false; 18718 return SetPropertyResult.success; 18719 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 18720 import core.stdc.stdlib; 18721 char[128] zero = 0; 18722 if(buffer.length + 1 >= zero.length) 18723 return SetPropertyResult.wrongFormat; 18724 zero[0 .. buffer.length] = buffer[]; 18725 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 18726 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 18727 import core.stdc.stdlib; 18728 char[128] zero = 0; 18729 if(buffer.length + 1 >= zero.length) 18730 return SetPropertyResult.wrongFormat; 18731 zero[0 .. buffer.length] = buffer[]; 18732 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 18733 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 18734 __traits(getMember, this, memberName) = value.idup; 18735 } else { 18736 return SetPropertyResult.notImplemented; 18737 } 18738 18739 } 18740 default: 18741 return super.setPropertyFromString(name, value, valueIsJson); 18742 } 18743 } 18744 } 18745 18746 /++ 18747 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 18748 18749 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 18750 18751 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 18752 rarely need to use these building blocks directly. 18753 +/ 18754 mixin template RegisterGetters() { 18755 override void getPropertiesList(scope void delegate(string name) sink) const { 18756 super.getPropertiesList(sink); 18757 18758 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18759 sink(memberName); 18760 } 18761 } 18762 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 18763 switch(name) { 18764 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18765 case memberName: 18766 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 18767 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 18768 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 18769 import core.stdc.stdio; 18770 char[32] buffer; 18771 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 18772 sink(name, buffer[0 .. len], true); 18773 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 18774 import core.stdc.stdio; 18775 char[32] buffer; 18776 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 18777 sink(name, buffer[0 .. len], true); 18778 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 18779 sink(name, __traits(getMember, this, memberName), false); 18780 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 18781 } else { 18782 sink(name, null, true); 18783 } 18784 18785 return; 18786 } 18787 default: 18788 return super.getPropertyAsString(name, sink); 18789 } 18790 } 18791 } 18792 } 18793 18794 private struct Stack(T) { 18795 this(int maxSize) { 18796 internalLength = 0; 18797 arr = initialBuffer[]; 18798 } 18799 18800 ///. 18801 void push(T t) { 18802 if(internalLength >= arr.length) { 18803 auto oldarr = arr; 18804 if(arr.length < 4096) 18805 arr = new T[arr.length * 2]; 18806 else 18807 arr = new T[arr.length + 4096]; 18808 arr[0 .. oldarr.length] = oldarr[]; 18809 } 18810 18811 arr[internalLength] = t; 18812 internalLength++; 18813 } 18814 18815 ///. 18816 T pop() { 18817 assert(internalLength); 18818 internalLength--; 18819 return arr[internalLength]; 18820 } 18821 18822 ///. 18823 T peek() { 18824 assert(internalLength); 18825 return arr[internalLength - 1]; 18826 } 18827 18828 ///. 18829 @property bool empty() { 18830 return internalLength ? false : true; 18831 } 18832 18833 ///. 18834 private T[] arr; 18835 private size_t internalLength; 18836 private T[64] initialBuffer; 18837 // 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), 18838 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 18839 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 18840 } 18841 18842 /// 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. 18843 private struct WidgetStream { 18844 18845 ///. 18846 @property Widget front() { 18847 return current.widget; 18848 } 18849 18850 /// Use Widget.tree instead. 18851 this(Widget start) { 18852 current.widget = start; 18853 current.childPosition = -1; 18854 isEmpty = false; 18855 stack = typeof(stack)(0); 18856 } 18857 18858 /* 18859 Handle it 18860 handle its children 18861 18862 */ 18863 18864 ///. 18865 void popFront() { 18866 more: 18867 if(isEmpty) return; 18868 18869 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 18870 18871 current.childPosition++; 18872 if(current.childPosition >= current.widget.children.length) { 18873 if(stack.empty()) 18874 isEmpty = true; 18875 else { 18876 current = stack.pop(); 18877 goto more; 18878 } 18879 } else { 18880 stack.push(current); 18881 current.widget = current.widget.children[current.childPosition]; 18882 current.childPosition = -1; 18883 } 18884 } 18885 18886 ///. 18887 @property bool empty() { 18888 return isEmpty; 18889 } 18890 18891 private: 18892 18893 struct Current { 18894 Widget widget; 18895 int childPosition; 18896 } 18897 18898 Current current; 18899 18900 Stack!(Current) stack; 18901 18902 bool isEmpty; 18903 } 18904 18905 18906 /+ 18907 18908 I could fix up the hierarchy kinda like this 18909 18910 class Widget { 18911 Widget[] children() { return null; } 18912 } 18913 interface WidgetContainer { 18914 Widget asWidget(); 18915 void addChild(Widget w); 18916 18917 // alias asWidget this; // but meh 18918 } 18919 18920 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 18921 18922 class Layout : Widget, WidgetContainer {} 18923 18924 class Window : WidgetContainer {} 18925 18926 18927 All constructors that previously took Widgets should now take WidgetContainers instead 18928 18929 18930 18931 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". 18932 +/ 18933 18934 /+ 18935 LAYOUTS 2.0 18936 18937 can just be assigned as a function. assigning a new one will cause it to be immediately called. 18938 18939 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 18940 18941 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 18942 18943 and even Paint can just use computedStyle... 18944 18945 background color 18946 font 18947 border color and style 18948 18949 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 18950 please note that many widgets and in some modes will completely ignore properties as they will. 18951 they are just hints you set, not promises. 18952 18953 18954 18955 18956 18957 So generally the existing virtual functions are just the default for the class. But individual objects 18958 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 18959 +/ 18960 18961 /++ 18962 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. 18963 18964 History: 18965 Added May 24, 2021. 18966 +/ 18967 struct WidgetBackground { 18968 /++ 18969 A background with the given solid color. 18970 +/ 18971 this(Color color) { 18972 this.color = color; 18973 } 18974 18975 this(WidgetBackground bg) { 18976 this = bg; 18977 } 18978 18979 /++ 18980 Creates a widget from the string. 18981 18982 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 18983 +/ 18984 static WidgetBackground fromString(string s) { 18985 return WidgetBackground(Color.fromString(s)); 18986 } 18987 18988 /++ 18989 The background is not necessarily a solid color, but you can always specify a color as a fallback. 18990 18991 History: 18992 Made `public` on December 18, 2022 (dub v10.10). 18993 +/ 18994 Color color; 18995 } 18996 18997 /++ 18998 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!) 18999 19000 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. 19001 19002 You should not inherit from this directly, but instead use [VisualTheme]. 19003 19004 History: 19005 Added May 8, 2021 19006 +/ 19007 abstract class BaseVisualTheme { 19008 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 19009 abstract void doPaint(Widget widget, WidgetPainter painter); 19010 19011 /+ 19012 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 19013 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 19014 +/ 19015 19016 /++ 19017 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 19018 where the interpretation of the string varies for each property and may include things like measurement units. 19019 +/ 19020 abstract string getPropertyString(Widget widget, string propertyName); 19021 19022 /++ 19023 Default background color of the window. Widgets also use this to simulate transparency. 19024 19025 Probably some shade of grey. 19026 +/ 19027 abstract Color windowBackgroundColor(); 19028 abstract Color widgetBackgroundColor(); 19029 abstract Color foregroundColor(); 19030 abstract Color lightAccentColor(); 19031 abstract Color darkAccentColor(); 19032 19033 /++ 19034 Colors used to indicate active selections in lists and text boxes, etc. 19035 +/ 19036 abstract Color selectionForegroundColor(); 19037 /// ditto 19038 abstract Color selectionBackgroundColor(); 19039 19040 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 19041 19042 /++ 19043 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 19044 +/ 19045 abstract OperatingSystemFont defaultFont(int dpi); 19046 19047 private OperatingSystemFont[int] defaultFontCache_; 19048 private OperatingSystemFont defaultFontCached(int dpi) { 19049 if(dpi !in defaultFontCache_) { 19050 // FIXME: set this to false if X disconnect or if visual theme changes 19051 defaultFontCache_[dpi] = defaultFont(dpi); 19052 } 19053 return defaultFontCache_[dpi]; 19054 } 19055 } 19056 19057 /+ 19058 A widget should have: 19059 classList 19060 dataset 19061 attributes 19062 computedStyles 19063 state (persistent) 19064 dynamic state (focused, hover, etc) 19065 +/ 19066 19067 // visualTheme.computedStyle(this).paddingLeft 19068 19069 19070 /++ 19071 This is your entry point to create your own visual theme for custom widgets. 19072 19073 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 19074 19075 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 19076 +/ 19077 abstract class VisualTheme(CRTP) : BaseVisualTheme { 19078 override string getPropertyString(Widget widget, string propertyName) { 19079 return null; 19080 } 19081 19082 /+ 19083 mixin StyleOverride!Widget 19084 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 19085 w.useStyleProperties(dg); 19086 } 19087 +/ 19088 19089 final override void doPaint(Widget widget, WidgetPainter painter) { 19090 auto derived = cast(CRTP) cast(void*) this; 19091 19092 scope void delegate(Widget, WidgetPainter) bestMatch; 19093 int bestMatchScore; 19094 19095 static if(__traits(hasMember, CRTP, "paint")) 19096 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 19097 static if(is(typeof(overload) Params == __parameters)) { 19098 static assert(Params.length == 2); 19099 static assert(is(Params[0] : Widget)); 19100 static assert(is(Params[1] == WidgetPainter)); 19101 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); 19102 19103 alias type = Params[0]; 19104 if(cast(type) widget) { 19105 auto score = baseClassCount!type; 19106 19107 if(score > bestMatchScore) { 19108 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 19109 bestMatchScore = score; 19110 } 19111 } 19112 } else static assert(0, "paint should be a method."); 19113 } 19114 19115 if(bestMatch) 19116 bestMatch(widget, painter); 19117 else 19118 widget.paint(painter); 19119 } 19120 19121 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 19122 19123 // 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 19124 // mixin Beautiful95Theme; 19125 mixin DefaultLightTheme; 19126 19127 private static struct Cached { 19128 // i prolly want to do this 19129 } 19130 } 19131 19132 /// ditto 19133 mixin template Beautiful95Theme() { 19134 override Color windowBackgroundColor() { return Color(212, 212, 212); } 19135 override Color widgetBackgroundColor() { return Color.white; } 19136 override Color foregroundColor() { return Color.black; } 19137 override Color darkAccentColor() { return Color(172, 172, 172); } 19138 override Color lightAccentColor() { return Color(223, 223, 223); } 19139 override Color selectionForegroundColor() { return Color.white; } 19140 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 19141 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 19142 } 19143 19144 /// ditto 19145 mixin template DefaultLightTheme() { 19146 override Color windowBackgroundColor() { return Color(232, 232, 232); } 19147 override Color widgetBackgroundColor() { return Color.white; } 19148 override Color foregroundColor() { return Color.black; } 19149 override Color darkAccentColor() { return Color(172, 172, 172); } 19150 override Color lightAccentColor() { return Color(223, 223, 223); } 19151 override Color selectionForegroundColor() { return Color.white; } 19152 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 19153 override OperatingSystemFont defaultFont(int dpi) { 19154 version(Windows) 19155 return new OperatingSystemFont("Segoe UI"); 19156 else static if(UsingSimpledisplayCocoa) { 19157 return (new OperatingSystemFont()).loadDefault; 19158 } else { 19159 // FIXME: undo xft's scaling so we don't end up double scaled 19160 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 19161 } 19162 } 19163 } 19164 19165 /// ditto 19166 mixin template DefaultDarkTheme() { 19167 override Color windowBackgroundColor() { return Color(64, 64, 64); } 19168 override Color widgetBackgroundColor() { return Color.black; } 19169 override Color foregroundColor() { return Color.white; } 19170 override Color darkAccentColor() { return Color(20, 20, 20); } 19171 override Color lightAccentColor() { return Color(80, 80, 80); } 19172 override Color selectionForegroundColor() { return Color.white; } 19173 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 19174 override OperatingSystemFont defaultFont(int dpi) { 19175 version(Windows) 19176 return new OperatingSystemFont("Segoe UI", 12); 19177 else static if(UsingSimpledisplayCocoa) { 19178 return (new OperatingSystemFont()).loadDefault; 19179 } else { 19180 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 19181 } 19182 } 19183 } 19184 19185 /// ditto 19186 alias DefaultTheme = DefaultLightTheme; 19187 19188 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 19189 /+ 19190 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 19191 Color windowBackgroundColor() { return Color(242, 242, 242); } 19192 Color darkAccentColor() { return windowBackgroundColor; } 19193 Color lightAccentColor() { return windowBackgroundColor; } 19194 +/ 19195 } 19196 19197 /++ 19198 Event fired when an [Observable] variable changes. You will want to add an event listener referencing 19199 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 19200 19201 History: 19202 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 19203 19204 Made `final` on January 3, 2025 19205 +/ 19206 final class StateChanged(alias field) : Event { 19207 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 19208 override bool cancelable() const { return false; } 19209 this(Widget target, typeof(field) newValue) { 19210 this.newValue = newValue; 19211 super(EventString, target); 19212 } 19213 19214 typeof(field) newValue; 19215 } 19216 19217 /++ 19218 Convenience function to add a `triggered` event listener. 19219 19220 Its implementation is simply `w.addEventListener("triggered", dg);` 19221 19222 History: 19223 Added November 27, 2021 (dub v10.4) 19224 +/ 19225 void addWhenTriggered(Widget w, void delegate() dg) { 19226 w.addEventListener("triggered", dg); 19227 } 19228 19229 /++ 19230 Observable variables can be added to widgets and when they are changed, it fires 19231 off a [StateChanged] event so you can react to it. 19232 19233 It is implemented as a getter and setter property, along with another helper you 19234 can use to subscribe with is `name_changed`. You can also subscribe to the [StateChanged] 19235 event through the usual means. Just give the name of the variable. See [StateChanged] for an 19236 example. 19237 19238 To get an `ObservableReference` to the observable, use `&yourname_changed`. 19239 19240 History: 19241 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 19242 19243 As of March 5, 2025, the changed function now returns an [EventListener] handle, which 19244 you can use to disconnect the observer. 19245 +/ 19246 mixin template Observable(T, string name) { 19247 private T backing; 19248 19249 mixin(q{ 19250 EventListener } ~ name ~ q{_changed (void delegate(T) dg) { 19251 return this.addEventListener((StateChanged!this_thing ev) { 19252 dg(ev.newValue); 19253 }); 19254 } 19255 19256 @property T } ~ name ~ q{ () { 19257 return backing; 19258 } 19259 19260 @property void } ~ name ~ q{ (T t) { 19261 backing = t; 19262 auto event = new StateChanged!this_thing(this, t); 19263 event.dispatch(); 19264 } 19265 }); 19266 19267 mixin("private alias this_thing = " ~ name ~ ";"); 19268 } 19269 19270 /// ditto 19271 alias ObservableReference(T) = EventListener delegate(void delegate(T)); 19272 19273 private bool startsWith(string test, string thing) { 19274 if(test.length < thing.length) 19275 return false; 19276 return test[0 .. thing.length] == thing; 19277 } 19278 19279 private bool endsWith(string test, string thing) { 19280 if(test.length < thing.length) 19281 return false; 19282 return test[$ - thing.length .. $] == thing; 19283 } 19284 19285 /++ 19286 Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon` 19287 19288 Note they can NOT have accelerators or toolbars; those annotations will be ignored. 19289 19290 Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time. 19291 19292 See_Also: 19293 [Widget.setMenuAndToolbarFromAnnotatedCode] 19294 +/ 19295 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) { 19296 return createContextMenuFromAnnotatedCode(w, w); 19297 } 19298 19299 /// ditto 19300 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) { 19301 return createContextMenuFromAnnotatedCode_internal(w, t); 19302 } 19303 /// ditto 19304 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) { 19305 return createContextMenuFromAnnotatedCode_internal(w, t); 19306 } 19307 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) { 19308 Menu ret = new Menu("", w); 19309 19310 foreach(memberName; __traits(derivedMembers, T)) { 19311 static if(memberName != "this") 19312 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 19313 .menu menu; 19314 bool separator; 19315 .hotkey hotkey; 19316 .icon icon; 19317 string label; 19318 string tip; 19319 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 19320 static if(is(typeof(attr) == .menu)) 19321 menu = attr; 19322 else static if(is(attr == .separator)) 19323 separator = true; 19324 else static if(is(typeof(attr) == .hotkey)) 19325 hotkey = attr; 19326 else static if(is(typeof(attr) == .icon)) 19327 icon = attr; 19328 else static if(is(typeof(attr) == .label)) 19329 label = attr.label; 19330 else static if(is(typeof(attr) == .tip)) 19331 tip = attr.tip; 19332 } 19333 19334 if(menu is .menu.init) { 19335 ushort correctIcon = icon.id; // FIXME 19336 if(label.length == 0) 19337 label = memberName.toMenuLabel; 19338 19339 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName)); 19340 19341 auto action = new Action(label, correctIcon, handler); 19342 19343 if(separator) 19344 ret.addSeparator(); 19345 ret.addItem(new MenuItem(action)); 19346 } 19347 } 19348 } 19349 19350 return ret; 19351 } 19352 19353 // still do layout delegation 19354 // and... split off Window from Widget. 19355 19356 version(minigui_screenshots) 19357 struct Screenshot { 19358 string name; 19359 } 19360 19361 version(minigui_screenshots) 19362 static if(__VERSION__ > 2092) 19363 mixin(q{ 19364 shared static this() { 19365 import core.runtime; 19366 19367 static UnitTestResult screenshotMagic() { 19368 string name; 19369 19370 import arsd.png; 19371 19372 auto results = new Window(); 19373 auto button = new Button("do it", results); 19374 19375 Window.newWindowCreated = delegate(Window w) { 19376 Timer timer; 19377 timer = new Timer(250, { 19378 auto img = w.win.takeScreenshot(); 19379 timer.destroy(); 19380 19381 version(Windows) 19382 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 19383 else 19384 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 19385 19386 w.close(); 19387 }); 19388 }; 19389 19390 button.addWhenTriggered( { 19391 19392 foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) { 19393 name = null; 19394 static foreach(attr; __traits(getAttributes, test)) { 19395 static if(is(typeof(attr) == Screenshot)) 19396 name = attr.name; 19397 } 19398 if(name.length) { 19399 test(); 19400 } 19401 } 19402 19403 }); 19404 19405 results.loop(); 19406 19407 return UnitTestResult(0, 0, false, false); 19408 } 19409 19410 19411 Runtime.extendedModuleUnitTester = &screenshotMagic; 19412 } 19413 }); 19414 version(minigui_screenshots) { 19415 version(unittest) 19416 void main() {} 19417 else static assert(0, "dont forget the -unittest flag to dmd"); 19418 } 19419 19420 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 19421 // FIXME: make multiple accelerators disambiguate based ona rgs 19422 // FIXME: MainWindow ctor should have same arg order as Window 19423 // FIXME: mainwindow ctor w/ client area size instead of total size. 19424 // 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. 19425 // FIXME: tri-state checkbox 19426 // FIXME: subordinate controls grouping...