-
Notifications
You must be signed in to change notification settings - Fork 128
/
minigui.d
16802 lines (13861 loc) · 478 KB
/
minigui.d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
// 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
// me@arsd:~/.kde/share/config$ vim kdeglobals
// FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too.
// https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/
// for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever.
// responsive minigui, menu search, and file open with a preview hook on the side.
// FIXME: add menu checkbox and menu icon eventually
/*
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
the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it...
*/
// FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places
// FIXME: text label must be copyable to the clipboard, at least as a full chunk.
// FIXME: opt-in file picker widget with image support
// FIXME: number widget
// https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm
// https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles
// osx style menu search.
// would be cool for a scroll bar to have marking capabilities
// kinda like vim's marks just on clicks etc and visual representation
// generically. may be cool to add an up arrow to the bottom too
//
// leave a shadow of where you last were for going back easily
// So a window needs to have a selection, and that can be represented by a type. This is manipulated by various
// functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for
// the window.
// so what about context menus?
// https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
// FIXME: make the scroll thing go to bottom when the content changes.
// 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
// FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
// FIXME: add a command search thingy built in and implement tip.
// FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!
// On Windows:
// FIXME: various labels look broken in high contrast mode
// FIXME: changing themes while the program is upen doesn't trigger a redraw
// add note about manifest to documentation. also icons.
// a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar
// FIXME: clear the corner of scrollbars if they pop up
// minigui needs to have a stdout redirection for gui mode on windows writeln
// I kinda wanna do state reacting. sort of. idk tho
// need a viewer widget that works like a web page - arrows scroll down consistently
// I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside.
// FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two.
// and help info about menu items.
// and search in menus?
// FIXME: a scroll area event signaling when a thing comes into view might be good
// FIXME: arrow key navigation and accelerators in dialog boxes will be a must
// FIXME: unify Windows style line endings
/*
TODO:
pie menu
class Form with submit behavior -- see AutomaticDialog
disabled widgets and menu items
event cleanup
tooltips.
api improvements
margins are kinda broken, they don't collapse like they should. at least.
a table form btw would be a horizontal layout of vertical layouts holding each column
that would give the same width things
*/
/*
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
*/
/++
minigui is a smallish GUI widget library, aiming to be on par with at least
HTML4 forms and a few other expected gui components. It uses native controls
on Windows and does its own thing on Linux (Mac is not currently supported but
may be later, and should use native controls) to keep size down. The Linux
appearance is similar to Windows 95 and avoids using images to maintain network
efficiency on remote X connections, though you can customize that.
minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color],
on which it is built. simpledisplay provides the low-level interfaces and minigui
builds the concept of widgets inside the windows on top of it.
Its #1 goal is to be useful without being large and complicated like GTK and Qt.
It isn't hugely concerned with appearance - on Windows, it just uses the native
controls and native theme, and on Linux, it keeps it simple and I may change that
at any time, though after May 2021, you can customize some things with css-inspired
[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
you can use the custom implementation there too, but... you shouldn't.)
The event model is similar to what you use in the browser with Javascript and the
layout engine tries to automatically fit things in, similar to a css flexbox.
FOR BEST RESULTS: be sure to link with the appropriate subsystem command
`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
Otherwise you'll get a console and possibly other visual bugs. But if you do use
the subsystem:windows, note that Phobos' writeln will crash the program!
HTML_To_Classes:
$(SMALL_TABLE
HTML Code | Minigui Class
`<input type="text">` | [LineEdit]
`<textarea>` | [TextEdit]
`<select>` | [DropDownSelection]
`<input type="checkbox">` | [Checkbox]
`<input type="radio">` | [Radiobox]
`<button>` | [Button]
)
Stretchiness:
The default is 4. You can use larger numbers for things that should
consume a lot of space, and lower numbers for ones that are better at
smaller sizes.
Overlapped_input:
COMING EVENTUALLY:
minigui will include a little bit of I/O functionality that just works
with the event loop. If you want to get fancy, I suggest spinning up
another thread and posting events back and forth.
$(H2 Add ons)
See the `minigui_addons` directory in the arsd repo for some add on widgets
you can import separately too.
$(H3 XML definitions)
If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
$(H3 Scriptability)
minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
in this documentation, it means you can call it from the script language.
Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
---
import arsd.minigui_xml;
import arsd.script;
var globals = var.emptyObject;
globals.makeWidgetFromString = &makeWidgetFromString;
// this now works
interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
---
More to come.
History:
Minigui had mostly additive changes or bug fixes since its inception until May 2021.
In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
tag this as version 2.0.
Among the changes:
$(LIST
* 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.
See [Event] for details.
* 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.
See [DoubleClickEvent] for details.
* 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.
See [Widget.Style] for details.
// * A widget must now opt in to receiving keyboard focus, rather than opting out.
* 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.
* 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.
* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
* 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.
* Various non-breaking additions.
)
+/
module arsd.minigui;
/++
This hello world sample will have an oversized button, but that's ok, you see your first window!
+/
version(Demo)
unittest {
import arsd.minigui;
void main() {
auto window = new MainWindow();
// note the parent widget is almost always passed as the last argument to a constructor
auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
auto button = new Button("Close", window);
button.addWhenTriggered({
window.close();
});
window.loop();
}
main(); // exclude from docs
}
/++
This example shows one way you can partition your window into a header
and sidebar. Here, the header and sidebar have a fixed width, while the
rest of the content sizes with the window.
It might be a new way of thinking about window layout to do things this
way - perhaps [GridLayout] more matches your style of thought - but the
concept here is to partition the window into sub-boxes with a particular
size, then partition those boxes into further boxes.
$(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.)
So to make the header, start with a child layout that has a max height.
It will use that space from the top, then the remaining children will
split the remaining area, meaning you can think of is as just being another
box you can split again. Keep splitting until you have the look you desire.
+/
// https://github.com/adamdruppe/arsd/issues/310
version(minigui_screenshots)
@Screenshot("layout")
unittest {
import arsd.minigui;
// This helper class is just to help make the layout boxes visible.
// think of it like a <div style="background-color: whatever;"></div> in HTML.
class ColorWidget : Widget {
this(Color color, Widget parent) {
this.color = color;
super(parent);
}
Color color;
class Style : Widget.Style {
override WidgetBackground background() { return WidgetBackground(color); }
}
mixin OverrideStyle!Style;
}
void main() {
auto window = new Window;
// the key is to give it a max height. This is one way to do it:
auto header = new class HorizontalLayout {
this() { super(window); }
override int maxHeight() { return 50; }
};
// this next line is a shortcut way of doing it too, but it only works
// for HorizontalLayout and VerticalLayout, and is less explicit, so it
// is good to know how to make a new class like above anyway.
// auto header = new HorizontalLayout(50, window);
auto bar = new HorizontalLayout(window);
// or since this is so common, VerticalLayout and HorizontalLayout both
// can just take an argument in their constructor for max width/height respectively
// (could have tone this above too, but I wanted to demo both techniques)
auto left = new VerticalLayout(100, bar);
// and this is the main section's container. A plain Widget instance is good enough here.
auto container = new Widget(bar);
// and these just add color to the containers we made above for the screenshot.
// in a real application, you can just add your actual controls instead of these.
auto headerColorBox = new ColorWidget(Color.teal, header);
auto leftColorBox = new ColorWidget(Color.green, left);
auto rightColorBox = new ColorWidget(Color.purple, container);
window.loop();
}
main(); // exclude from docs
}
import arsd.core;
alias Timer = arsd.simpledisplay.Timer;
public import arsd.simpledisplay;
/++
Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
History:
Was private until May 15, 2021.
+/
public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
version(Windows) {
import core.sys.windows.winnls;
import core.sys.windows.windef;
import core.sys.windows.basetyps;
import core.sys.windows.winbase;
import core.sys.windows.winuser;
import core.sys.windows.wingdi;
static import gdi = core.sys.windows.wingdi;
}
version(Windows) {
version(minigui_manifest) {} else version=minigui_no_manifest;
version(minigui_no_manifest) {} else
static if(__VERSION__ >= 2_083)
version(CRuntime_Microsoft) { // FIXME: mingw?
// assume we want commctrl6 whenever possible since there's really no reason not to
// and this avoids some of the manifest hassle
pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
}
}
// this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
private bool lastDefaultPrevented;
/// Methods marked with this are available from scripts if added to the [arsd.script] engine.
alias scriptable = arsd_jsvar_compatible;
version(Windows) {
// use native widgets when available unless specifically asked otherwise
version(custom_widgets) {
enum bool UsingCustomWidgets = true;
enum bool UsingWin32Widgets = false;
} else {
version = win32_widgets;
enum bool UsingCustomWidgets = false;
enum bool UsingWin32Widgets = true;
// give access to my text system for the rich text cross platform stuff
version = use_new_text_system;
import arsd.textlayouter;
}
// and native theming when needed
//version = win32_theming;
} else {
enum bool UsingCustomWidgets = true;
enum bool UsingWin32Widgets = false;
version=custom_widgets;
}
/*
The main goals of minigui.d are to:
1) Provide basic widgets that just work in a lightweight lib.
I basically want things comparable to a plain HTML form,
plus the easy and obvious things you expect from Windows
apps like a menu.
2) Use native things when possible for best functionality with
least library weight.
3) Give building blocks to provide easy extension for your
custom widgets, or hooking into additional native widgets
I didn't wrap.
4) Provide interfaces for easy interaction between third
party minigui extensions. (event model, perhaps
signals/slots, drop-in ease of use bits.)
5) Zero non-system dependencies, including Phobos as much as
I reasonably can. It must only import arsd.color and
my simpledisplay.d. If you need more, it will have to be
an extension module.
6) An easy layout system that generally works.
A stretch goal is to make it easy to make gui forms with code,
some kind of resource file (xml?) and even a wysiwyg designer.
Another stretch goal is to make it easy to hook data into the gui,
including from reflection. So like auto-generate a form from a
function signature or struct definition, or show a list from an
array that automatically updates as the array is changed. Then,
your program focuses on the data more than the gui interaction.
STILL NEEDED:
* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
* slider
* listbox
* spinner
* label?
* rich text
*/
/+
enum LayoutMethods {
verticalFlex,
horizontalFlex,
inlineBlock, // left to right, no stretch, goes to next line as needed
static, // just set to x, y
verticalNoStretch, // browser style default
inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
grid, // magic
}
+/
/++
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.
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.
---
class MinimalWidget : Widget {
this(Widget parent) {
super(parent);
}
}
---
$(SIDEBAR
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.
)
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.
Among the things you'll most likely want to change in your custom widget:
$(LIST
* 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.)
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.
Do this $(I after) calling the `super` constructor.
* 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.
Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
* 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.
* 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.
)
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.
It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
Your own custom-drawn and native system controls can exist side-by-side.
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.
+/
class Widget : ReflectableProperties {
private bool willDraw() {
return true;
}
/+
/++
Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
History:
Added September 15, 2021
implemented.... ???
+/
void prepareReflection(this This)() {
}
+/
private bool _enabled = true;
/++
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.
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.
History:
Added November 23, 2021 (dub v10.4)
Warning: the specific behavior of disabling with parents may change in the future.
Bugs:
Currently only implemented for widgets backed by native Windows controls.
See_Also: [disabledReason], [disabledBy]
+/
@property bool enabled() {
return disabledBy() is null;
}
/// ditto
@property void enabled(bool yes) {
_enabled = yes;
version(win32_widgets) {
if(hwnd)
EnableWindow(hwnd, yes);
}
setDynamicState(DynamicState.disabled, yes);
}
private string disabledReason_;
/++
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.
Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
History:
Added November 23, 2021 (dub v10.4)
See_Also: [enabled], [disabledBy]
+/
@property string disabledReason() {
auto w = disabledBy();
return (w is null) ? null : w.disabledReason_;
}
/// ditto
@property void disabledReason(string reason) {
disabledReason_ = reason;
}
/++
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.
History:
Added November 25, 2021 (dub v10.4)
See_Also: [enabled], [disabledReason]
+/
Widget disabledBy() {
Widget p = this;
while(p) {
if(!p._enabled)
return p;
p = p.parent;
}
return null;
}
/// Implementations of [ReflectableProperties] interface. See the interface for details.
SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
if(valueIsJson)
return SetPropertyResult.wrongFormat;
switch(name) {
case "name":
this.name = value.idup;
return SetPropertyResult.success;
case "statusTip":
this.statusTip = value.idup;
return SetPropertyResult.success;
default:
return SetPropertyResult.noSuchProperty;
}
}
/// ditto
void getPropertiesList(scope void delegate(string name) sink) const {
sink("name");
sink("statusTip");
}
/// ditto
void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
switch(name) {
case "name":
sink(name, this.name, false);
return;
case "statusTip":
sink(name, this.statusTip, false);
return;
default:
sink(name, null, true);
}
}
/++
Scales the given value to the system-reported DPI for the monitor on which the widget resides.
History:
Added November 25, 2021 (dub v10.5)
`Point` overload added January 12, 2022 (dub v10.6)
+/
int scaleWithDpi(int value, int assumedDpi = 96) {
// avoid potential overflow with common special values
if(value == int.max)
return int.max;
if(value == int.min)
return int.min;
if(value == 0)
return 0;
return value * currentDpi(assumedDpi) / assumedDpi;
}
/// ditto
Point scaleWithDpi(Point value, int assumedDpi = 96) {
return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
}
/++
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.
Not entirely stable.
History:
Added August 25, 2023 (dub v11.1)
+/
final int currentDpi(int assumedDpi = 96) {
// assert(parentWindow !is null);
// assert(parentWindow.win !is null);
auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
//divide = 138; // to test 1.5x
// 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.
// this also covers the case when actualDpi returns 0.
if(divide < 96)
divide = 96;
return divide;
}
// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
// I'll think up something better eventually
// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
protected final int defaultLineHeight() {
auto cs = getComputedStyle();
if(cs.font && !cs.font.isNull)
return cs.font.height() * 5 / 4;
else
return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
}
/++
History:
Added August 25, 2023 (dub v11.1)
+/
protected final int defaultTextHeight(int numberOfLines = 1) {
auto cs = getComputedStyle();
if(cs.font && !cs.font.isNull)
return cs.font.height() * numberOfLines;
else
return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
}
protected final int defaultTextWidth(const(char)[] text) {
auto cs = getComputedStyle();
if(cs.font && !cs.font.isNull)
return cs.font.stringWidth(text);
else
return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
}
/++
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.
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.
History:
Added May 22, 2021
+/
protected bool encapsulatedChildren() {
return false;
}
private void privateDpiChanged() {
dpiChanged();
foreach(child; children)
child.privateDpiChanged();
}
/++
Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
History:
Added January 12, 2022 (dub v10.6)
+/
protected void dpiChanged() {
}
// Default layout properties {
int minWidth() { return 0; }
int minHeight() {
// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
int sum = this.paddingTop + this.paddingBottom;
foreach(child; children) {
if(child.hidden)
continue;
sum += child.minHeight();
sum += child.marginTop();
sum += child.marginBottom();
}
return sum;
}
int maxWidth() { return int.max; }
int maxHeight() { return int.max; }
int widthStretchiness() { return 4; }
int heightStretchiness() { return 4; }
/++
Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
History:
Added June 15, 2021 (dub v10.1)
+/
int widthShrinkiness() { return 0; }
/// ditto
int heightShrinkiness() { return 0; }
/++
The initial size of the widget for layout calculations. Default is 0.
See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
History:
Added June 15, 2021 (dub v10.1)
+/
int flexBasisWidth() { return 0; }
/// ditto
int flexBasisHeight() { return 0; }
/++
Not stable.
Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
History:
Added January 5, 2023
+/
Rectangle defaultMargin;
/// ditto
Rectangle defaultPadding;
int marginLeft() { return scaleWithDpi(defaultMargin.left); }
int marginRight() { return scaleWithDpi(defaultMargin.right); }
int marginTop() { return scaleWithDpi(defaultMargin.top); }
int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
int paddingRight() { return scaleWithDpi(defaultPadding.right); }
int paddingTop() { return scaleWithDpi(defaultPadding.top); }
int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
private bool recomputeChildLayoutRequired = true;
private static class RecomputeEvent {}
private __gshared rce = new RecomputeEvent();
protected final void queueRecomputeChildLayout() {
recomputeChildLayoutRequired = true;
if(this.parentWindow) {
auto sw = this.parentWindow.win;
assert(sw !is null);
if(!sw.eventQueued!RecomputeEvent) {
sw.postEvent(rce);
// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
}
}
}
protected final void recomputeChildLayoutEntry() {
if(recomputeChildLayoutRequired) {
recomputeChildLayout();
recomputeChildLayoutRequired = false;
redraw();
} else {
// I still need to check the tree just in case one of them was queued up
// and the event came up here instead of there.
foreach(child; children)
child.recomputeChildLayoutEntry();
}
}
// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
void recomputeChildLayout() {
.recomputeChildLayout!"height"(this);
}
// }
/++
Returns the style's tag name string this object uses.
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.
This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
History:
Added May 10, 2021
+/
string styleTagName() const {
string n = typeid(this).name;
foreach_reverse(idx, ch; n)
if(ch == '.') {
n = n[idx + 1 .. $];
break;
}
return n;
}
/// API for the [styleClassList]
static struct ClassList {
private Widget widget;
///
void add(string s) {
widget.styleClassList_ ~= s;
}
///
void remove(string s) {
foreach(idx, s1; widget.styleClassList_)
if(s1 == s) {
widget.styleClassList_[idx] = widget.styleClassList_[$-1];
widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
widget.styleClassList_.assumeSafeAppend();
return;
}
}
/// Returns true if it was added, false if it was removed.
bool toggle(string s) {
if(contains(s)) {
remove(s);
return false;
} else {
add(s);
return true;
}
}
///
bool contains(string s) const {
foreach(s1; widget.styleClassList_)
if(s1 == s)
return true;
return false;
}
}
private string[] styleClassList_;
/++
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.
It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
History:
Added May 10, 2021
+/
inout(ClassList) styleClassList() inout {
return cast(inout(ClassList)) ClassList(cast() this);
}
/++
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.
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.
The upper 32 bits are available for your own extensions.
History:
Added May 10, 2021
+/
enum DynamicState : ulong {
focus = (1 << 0), /// the widget currently has the keyboard focus
hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!)
invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!)
checked = (1 << 4), /// the widget is toggleable and currently toggled on
selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
disabled = (1 << 6), /// the widget is currently unable to perform its designated task
indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
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.
USER_BEGIN = (1UL << 32),
}
// I want to add the primary and cancel styles to buttons at least at some point somehow.
/// ditto
@property ulong dynamicState() { return dynamicState_; }
/// ditto
@property ulong dynamicState(ulong newValue) {
if(dynamicState != newValue) {
auto old = dynamicState_;
dynamicState_ = newValue;
useStyleProperties((scope Widget.Style s) {
if(s.variesWithState(old ^ newValue))
redraw();
});
}
return dynamicState_;
}
/// ditto
void setDynamicState(ulong flags, bool state) {
auto ds = dynamicState_;
if(state)
ds |= flags;
else
ds &= ~flags;
dynamicState = ds;
}
private ulong dynamicState_;
deprecated("Use dynamic styles instead now") {
Color backgroundColor() { return backgroundColor_; }
void backgroundColor(Color c){ this.backgroundColor_ = c; }
MouseCursor cursor() { return GenericCursor.Default; }
} private Color backgroundColor_ = Color.transparent;
/++
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).
It is here so there can be a specificity switch.
See [OverrideStyle] for a helper function to use your own.
History:
Added May 11, 2021
+/
static class Style/* : StyleProperties*/ {
public Widget widget; // public because the mixin template needs access to it
/++
You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
History:
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.
+/
bool variesWithState(ulong dynamicStateFlags) {
version(win32_widgets) {
if(widget.hwnd)
return false;
}
return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
}
///
Color foregroundColor() {
return WidgetPainter.visualTheme.foregroundColor;
}
///
WidgetBackground background() {
// the default is a "transparent" background, which means
// it goes as far up as it can to get the color
if (widget.backgroundColor_ != Color.transparent)
return WidgetBackground(widget.backgroundColor_);
if (widget.parent)
return widget.parent.getComputedStyle.background;
return WidgetBackground(widget.backgroundColor_);
}
private static OperatingSystemFont fontCached_;