forked from OpenTTD/OpenTTD
/
widget.cpp
2906 lines (2552 loc) · 109 KB
/
widget.cpp
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
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file widget.cpp Handling of the default/simple widgets. */
#include "stdafx.h"
#include "company_func.h"
#include "window_gui.h"
#include "viewport_func.h"
#include "zoom_func.h"
#include "strings_func.h"
#include "transparency.h"
#include "core/geometry_func.hpp"
#include "settings_type.h"
#include "querystring_gui.h"
#include "table/sprites.h"
#include "table/strings.h"
#include "table/string_colours.h"
#include "safeguards.h"
/**
* Compute the vertical position of the draggable part of scrollbar
* @param sb Scrollbar list data
* @param top Top position of the scrollbar (top position of the up-button)
* @param bottom Bottom position of the scrollbar (bottom position of the down-button)
* @param horizontal Whether the scrollbar is horizontal or not
* @return A Point, with x containing the top coordinate of the draggable part, and
* y containing the bottom coordinate of the draggable part
*/
static Point HandleScrollbarHittest(const Scrollbar *sb, int top, int bottom, bool horizontal)
{
/* Base for reversion */
int rev_base = top + bottom;
int button_size;
if (horizontal) {
button_size = NWidgetScrollbar::GetHorizontalDimension().width;
} else {
button_size = NWidgetScrollbar::GetVerticalDimension().height;
}
top += button_size; // top points to just below the up-button
bottom -= button_size; // bottom points to top of the down-button
int height = (bottom - top);
int pos = sb->GetPosition();
int count = sb->GetCount();
int cap = sb->GetCapacity();
if (count != 0) top += height * pos / count;
if (cap > count) cap = count;
if (count != 0) bottom -= (count - pos - cap) * height / count;
Point pt;
if (horizontal && _current_text_dir == TD_RTL) {
pt.x = rev_base - bottom;
pt.y = rev_base - top;
} else {
pt.x = top;
pt.y = bottom;
}
return pt;
}
/**
* Compute new position of the scrollbar after a click and updates the window flags.
* @param w Window on which a scroll was performed.
* @param sb Scrollbar
* @param mi Minimum coordinate of the scroll bar.
* @param ma Maximum coordinate of the scroll bar.
* @param x The X coordinate of the mouse click.
* @param y The Y coordinate of the mouse click.
*/
static void ScrollbarClickPositioning(Window *w, NWidgetScrollbar *sb, int x, int y, int mi, int ma)
{
int pos;
int button_size;
bool rtl = false;
if (sb->type == NWID_HSCROLLBAR) {
pos = x;
rtl = _current_text_dir == TD_RTL;
button_size = NWidgetScrollbar::GetHorizontalDimension().width;
} else {
pos = y;
button_size = NWidgetScrollbar::GetVerticalDimension().height;
}
if (pos < mi + button_size) {
/* Pressing the upper button? */
SetBit(sb->disp_flags, NDB_SCROLLBAR_UP);
if (_scroller_click_timeout <= 1) {
_scroller_click_timeout = 3;
sb->UpdatePosition(rtl ? 1 : -1);
}
w->mouse_capture_widget = sb->index;
} else if (pos >= ma - button_size) {
/* Pressing the lower button? */
SetBit(sb->disp_flags, NDB_SCROLLBAR_DOWN);
if (_scroller_click_timeout <= 1) {
_scroller_click_timeout = 3;
sb->UpdatePosition(rtl ? -1 : 1);
}
w->mouse_capture_widget = sb->index;
} else {
Point pt = HandleScrollbarHittest(sb, mi, ma, sb->type == NWID_HSCROLLBAR);
if (pos < pt.x) {
sb->UpdatePosition(rtl ? 1 : -1, Scrollbar::SS_BIG);
} else if (pos > pt.y) {
sb->UpdatePosition(rtl ? -1 : 1, Scrollbar::SS_BIG);
} else {
_scrollbar_start_pos = pt.x - mi - button_size;
_scrollbar_size = ma - mi - button_size * 2;
w->mouse_capture_widget = sb->index;
_cursorpos_drag_start = _cursor.pos;
}
}
w->SetDirty();
}
/**
* Special handling for the scrollbar widget type.
* Handles the special scrolling buttons and other scrolling.
* @param w Window on which a scroll was performed.
* @param nw Pointer to the scrollbar widget.
* @param x The X coordinate of the mouse click.
* @param y The Y coordinate of the mouse click.
*/
void ScrollbarClickHandler(Window *w, NWidgetCore *nw, int x, int y)
{
int mi, ma;
if (nw->type == NWID_HSCROLLBAR) {
mi = nw->pos_x;
ma = nw->pos_x + nw->current_x;
} else {
mi = nw->pos_y;
ma = nw->pos_y + nw->current_y;
}
NWidgetScrollbar *scrollbar = dynamic_cast<NWidgetScrollbar*>(nw);
assert(scrollbar != nullptr);
ScrollbarClickPositioning(w, scrollbar, x, y, mi, ma);
}
/**
* Returns the index for the widget located at the given position
* relative to the window. It includes all widget-corner pixels as well.
* @param *w Window to look inside
* @param x The Window client X coordinate
* @param y The Window client y coordinate
* @return A widget index, or -1 if no widget was found.
*/
int GetWidgetFromPos(const Window *w, int x, int y)
{
NWidgetCore *nw = w->nested_root->GetWidgetFromPos(x, y);
return (nw != nullptr) ? nw->index : -1;
}
/**
* Draw frame rectangle.
* @param left Left edge of the frame
* @param top Top edge of the frame
* @param right Right edge of the frame
* @param bottom Bottom edge of the frame
* @param colour Colour table to use. @see _colour_gradient
* @param flags Flags controlling how to draw the frame. @see FrameFlags
*/
void DrawFrameRect(int left, int top, int right, int bottom, Colours colour, FrameFlags flags)
{
assert(colour < COLOUR_END);
uint dark = _colour_gradient[colour][3];
uint medium_dark = _colour_gradient[colour][5];
uint medium_light = _colour_gradient[colour][6];
uint light = _colour_gradient[colour][7];
if (flags & FR_TRANSPARENT) {
GfxFillRect(left, top, right, bottom, PALETTE_TO_TRANSPARENT, FILLRECT_RECOLOUR);
} else {
uint interior;
if (flags & FR_LOWERED) {
GfxFillRect(left, top, left, bottom, dark);
GfxFillRect(left + WD_BEVEL_LEFT, top, right, top, dark);
GfxFillRect(right, top + WD_BEVEL_TOP, right, bottom - WD_BEVEL_BOTTOM, light);
GfxFillRect(left + WD_BEVEL_LEFT, bottom, right, bottom, light);
interior = (flags & FR_DARKENED ? medium_dark : medium_light);
} else {
GfxFillRect(left, top, left, bottom - WD_BEVEL_BOTTOM, light);
GfxFillRect(left + WD_BEVEL_LEFT, top, right - WD_BEVEL_RIGHT, top, light);
GfxFillRect(right, top, right, bottom - WD_BEVEL_BOTTOM, dark);
GfxFillRect(left, bottom, right, bottom, dark);
interior = medium_dark;
}
if (!(flags & FR_BORDERONLY)) {
GfxFillRect(left + WD_BEVEL_LEFT, top + WD_BEVEL_TOP, right - WD_BEVEL_RIGHT, bottom - WD_BEVEL_BOTTOM, interior);
}
}
}
/**
* Draw an image button.
* @param r Rectangle of the button.
* @param type Widget type (#WWT_IMGBTN or #WWT_IMGBTN_2).
* @param colour Colour of the button.
* @param clicked Button is lowered.
* @param img Sprite to draw.
*/
static inline void DrawImageButtons(const Rect &r, WidgetType type, Colours colour, bool clicked, SpriteID img)
{
assert(img != 0);
DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE);
if ((type & WWT_MASK) == WWT_IMGBTN_2 && clicked) img++; // Show different image when clicked for #WWT_IMGBTN_2.
Dimension d = GetSpriteSize(img);
DrawSprite(img, PAL_NONE, CenterBounds(r.left, r.right, d.width) + clicked, CenterBounds(r.top, r.bottom, d.height) + clicked);
}
/**
* Draw the label-part of a widget.
* @param r Rectangle of the label background.
* @param type Widget type (#WWT_TEXTBTN, #WWT_TEXTBTN_2, or #WWT_LABEL).
* @param clicked Label is rendered lowered.
* @param str Text to draw.
*/
static inline void DrawLabel(const Rect &r, WidgetType type, bool clicked, StringID str)
{
if (str == STR_NULL) return;
if ((type & WWT_MASK) == WWT_TEXTBTN_2 && clicked) str++;
Dimension d = GetStringBoundingBox(str);
int offset = max(0, ((int)(r.bottom - r.top + 1) - (int)d.height) / 2); // Offset for rendering the text vertically centered
DrawString(r.left + clicked, r.right + clicked, r.top + offset + clicked, str, TC_FROMSTRING, SA_HOR_CENTER);
}
/**
* Draw text.
* @param r Rectangle of the background.
* @param colour Colour of the text.
* @param str Text to draw.
*/
static inline void DrawText(const Rect &r, TextColour colour, StringID str)
{
Dimension d = GetStringBoundingBox(str);
int offset = max(0, ((int)(r.bottom - r.top + 1) - (int)d.height) / 2); // Offset for rendering the text vertically centered
if (str != STR_NULL) DrawString(r.left, r.right, r.top + offset, str, colour);
}
/**
* Draw an inset widget.
* @param r Rectangle of the background.
* @param colour Colour of the inset.
* @param str Text to draw.
*/
static inline void DrawInset(const Rect &r, Colours colour, StringID str)
{
DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, FR_LOWERED | FR_DARKENED);
if (str != STR_NULL) DrawString(r.left + WD_INSET_LEFT, r.right - WD_INSET_RIGHT, r.top + WD_INSET_TOP, str);
}
/**
* Draw a matrix widget.
* @param r Rectangle of the matrix background.
* @param colour Colour of the background.
* @param clicked Matrix is rendered lowered.
* @param data Data of the widget, number of rows and columns of the widget.
* @param resize_x Matrix resize unit size.
* @param resize_y Matrix resize unit size.
*/
static inline void DrawMatrix(const Rect &r, Colours colour, bool clicked, uint16 data, uint resize_x, uint resize_y)
{
DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE);
int num_columns = GB(data, MAT_COL_START, MAT_COL_BITS); // Lower 8 bits of the widget data: Number of columns in the matrix.
int column_width; // Width of a single column in the matrix.
if (num_columns == 0) {
column_width = resize_x;
num_columns = (r.right - r.left + 1) / column_width;
} else {
column_width = (r.right - r.left + 1) / num_columns;
}
int num_rows = GB(data, MAT_ROW_START, MAT_ROW_BITS); // Upper 8 bits of the widget data: Number of rows in the matrix.
int row_height; // Height of a single row in the matrix.
if (num_rows == 0) {
row_height = resize_y;
num_rows = (r.bottom - r.top + 1) / row_height;
} else {
row_height = (r.bottom - r.top + 1) / num_rows;
}
int col = _colour_gradient[colour & 0xF][6];
int x = r.left;
for (int ctr = num_columns; ctr > 1; ctr--) {
x += column_width;
GfxFillRect(x, r.top + 1, x, r.bottom - 1, col);
}
x = r.top;
for (int ctr = num_rows; ctr > 1; ctr--) {
x += row_height;
GfxFillRect(r.left + 1, x, r.right - 1, x, col);
}
col = _colour_gradient[colour & 0xF][4];
x = r.left - 1;
for (int ctr = num_columns; ctr > 1; ctr--) {
x += column_width;
GfxFillRect(x, r.top + 1, x, r.bottom - 1, col);
}
x = r.top - 1;
for (int ctr = num_rows; ctr > 1; ctr--) {
x += row_height;
GfxFillRect(r.left + 1, x, r.right - 1, x, col);
}
}
/**
* Draw a vertical scrollbar.
* @param r Rectangle of the scrollbar widget.
* @param colour Colour of the scrollbar widget.
* @param up_clicked Up-arrow is clicked.
* @param bar_dragged Bar is dragged.
* @param down_clicked Down-arrow is clicked.
* @param scrollbar Scrollbar size, offset, and capacity information.
*/
static inline void DrawVerticalScrollbar(const Rect &r, Colours colour, bool up_clicked, bool bar_dragged, bool down_clicked, const Scrollbar *scrollbar)
{
int centre = (r.right - r.left) / 2;
int height = NWidgetScrollbar::GetVerticalDimension().height;
/* draw up/down buttons */
DrawFrameRect(r.left, r.top, r.right, r.top + height - 1, colour, (up_clicked) ? FR_LOWERED : FR_NONE);
DrawSprite(SPR_ARROW_UP, PAL_NONE, r.left + 1 + up_clicked, r.top + 1 + up_clicked);
DrawFrameRect(r.left, r.bottom - (height - 1), r.right, r.bottom, colour, (down_clicked) ? FR_LOWERED : FR_NONE);
DrawSprite(SPR_ARROW_DOWN, PAL_NONE, r.left + 1 + down_clicked, r.bottom - (height - 2) + down_clicked);
int c1 = _colour_gradient[colour & 0xF][3];
int c2 = _colour_gradient[colour & 0xF][7];
/* draw "shaded" background */
GfxFillRect(r.left, r.top + height, r.right, r.bottom - height, c2);
GfxFillRect(r.left, r.top + height, r.right, r.bottom - height, c1, FILLRECT_CHECKER);
/* draw shaded lines */
GfxFillRect(r.left + centre - 3, r.top + height, r.left + centre - 3, r.bottom - height, c1);
GfxFillRect(r.left + centre - 2, r.top + height, r.left + centre - 2, r.bottom - height, c2);
GfxFillRect(r.left + centre + 2, r.top + height, r.left + centre + 2, r.bottom - height, c1);
GfxFillRect(r.left + centre + 3, r.top + height, r.left + centre + 3, r.bottom - height, c2);
Point pt = HandleScrollbarHittest(scrollbar, r.top, r.bottom, false);
DrawFrameRect(r.left, pt.x, r.right, pt.y, colour, bar_dragged ? FR_LOWERED : FR_NONE);
}
/**
* Draw a horizontal scrollbar.
* @param r Rectangle of the scrollbar widget.
* @param colour Colour of the scrollbar widget.
* @param left_clicked Left-arrow is clicked.
* @param bar_dragged Bar is dragged.
* @param right_clicked Right-arrow is clicked.
* @param scrollbar Scrollbar size, offset, and capacity information.
*/
static inline void DrawHorizontalScrollbar(const Rect &r, Colours colour, bool left_clicked, bool bar_dragged, bool right_clicked, const Scrollbar *scrollbar)
{
int centre = (r.bottom - r.top) / 2;
int width = NWidgetScrollbar::GetHorizontalDimension().width;
DrawFrameRect(r.left, r.top, r.left + width - 1, r.bottom, colour, left_clicked ? FR_LOWERED : FR_NONE);
DrawSprite(SPR_ARROW_LEFT, PAL_NONE, r.left + 1 + left_clicked, r.top + 1 + left_clicked);
DrawFrameRect(r.right - (width - 1), r.top, r.right, r.bottom, colour, right_clicked ? FR_LOWERED : FR_NONE);
DrawSprite(SPR_ARROW_RIGHT, PAL_NONE, r.right - (width - 2) + right_clicked, r.top + 1 + right_clicked);
int c1 = _colour_gradient[colour & 0xF][3];
int c2 = _colour_gradient[colour & 0xF][7];
/* draw "shaded" background */
GfxFillRect(r.left + width, r.top, r.right - width, r.bottom, c2);
GfxFillRect(r.left + width, r.top, r.right - width, r.bottom, c1, FILLRECT_CHECKER);
/* draw shaded lines */
GfxFillRect(r.left + width, r.top + centre - 3, r.right - width, r.top + centre - 3, c1);
GfxFillRect(r.left + width, r.top + centre - 2, r.right - width, r.top + centre - 2, c2);
GfxFillRect(r.left + width, r.top + centre + 2, r.right - width, r.top + centre + 2, c1);
GfxFillRect(r.left + width, r.top + centre + 3, r.right - width, r.top + centre + 3, c2);
/* draw actual scrollbar */
Point pt = HandleScrollbarHittest(scrollbar, r.left, r.right, true);
DrawFrameRect(pt.x, r.top, pt.y, r.bottom, colour, bar_dragged ? FR_LOWERED : FR_NONE);
}
/**
* Draw a frame widget.
* @param r Rectangle of the frame.
* @param colour Colour of the frame.
* @param str Text of the frame.
*/
static inline void DrawFrame(const Rect &r, Colours colour, StringID str)
{
int x2 = r.left; // by default the left side is the left side of the widget
if (str != STR_NULL) x2 = DrawString(r.left + WD_FRAMETEXT_LEFT, r.right - WD_FRAMETEXT_RIGHT, r.top, str);
int c1 = _colour_gradient[colour][3];
int c2 = _colour_gradient[colour][7];
/* If the frame has text, adjust the top bar to fit half-way through */
int dy1 = 4;
if (str != STR_NULL) dy1 = FONT_HEIGHT_NORMAL / 2 - 1;
int dy2 = dy1 + 1;
if (_current_text_dir == TD_LTR) {
/* Line from upper left corner to start of text */
GfxFillRect(r.left, r.top + dy1, r.left + 4, r.top + dy1, c1);
GfxFillRect(r.left + 1, r.top + dy2, r.left + 4, r.top + dy2, c2);
/* Line from end of text to upper right corner */
GfxFillRect(x2, r.top + dy1, r.right - 1, r.top + dy1, c1);
GfxFillRect(x2, r.top + dy2, r.right - 2, r.top + dy2, c2);
} else {
/* Line from upper left corner to start of text */
GfxFillRect(r.left, r.top + dy1, x2 - 2, r.top + dy1, c1);
GfxFillRect(r.left + 1, r.top + dy2, x2 - 2, r.top + dy2, c2);
/* Line from end of text to upper right corner */
GfxFillRect(r.right - 5, r.top + dy1, r.right - 1, r.top + dy1, c1);
GfxFillRect(r.right - 5, r.top + dy2, r.right - 2, r.top + dy2, c2);
}
/* Line from upper left corner to bottom left corner */
GfxFillRect(r.left, r.top + dy2, r.left, r.bottom - 1, c1);
GfxFillRect(r.left + 1, r.top + dy2 + 1, r.left + 1, r.bottom - 2, c2);
/* Line from upper right corner to bottom right corner */
GfxFillRect(r.right - 1, r.top + dy2, r.right - 1, r.bottom - 2, c1);
GfxFillRect(r.right, r.top + dy1, r.right, r.bottom - 1, c2);
GfxFillRect(r.left + 1, r.bottom - 1, r.right - 1, r.bottom - 1, c1);
GfxFillRect(r.left, r.bottom, r.right, r.bottom, c2);
}
/**
* Draw a shade box.
* @param r Rectangle of the box.
* @param colour Colour of the shade box.
* @param clicked Box is lowered.
*/
static inline void DrawShadeBox(const Rect &r, Colours colour, bool clicked)
{
DrawImageButtons(r, WWT_SHADEBOX, colour, clicked, clicked ? SPR_WINDOW_SHADE: SPR_WINDOW_UNSHADE);
}
/**
* Draw a sticky box.
* @param r Rectangle of the box.
* @param colour Colour of the sticky box.
* @param clicked Box is lowered.
*/
static inline void DrawStickyBox(const Rect &r, Colours colour, bool clicked)
{
DrawImageButtons(r, WWT_STICKYBOX, colour, clicked, clicked ? SPR_PIN_UP : SPR_PIN_DOWN);
}
/**
* Draw a defsize box.
* @param r Rectangle of the box.
* @param colour Colour of the defsize box.
* @param clicked Box is lowered.
*/
static inline void DrawDefSizeBox(const Rect &r, Colours colour, bool clicked)
{
DrawImageButtons(r, WWT_DEFSIZEBOX, colour, clicked, SPR_WINDOW_DEFSIZE);
}
/**
* Draw a NewGRF debug box.
* @param r Rectangle of the box.
* @param colour Colour of the debug box.
* @param clicked Box is lowered.
*/
static inline void DrawDebugBox(const Rect &r, Colours colour, bool clicked)
{
DrawImageButtons(r, WWT_DEBUGBOX, colour, clicked, SPR_WINDOW_DEBUG);
}
/**
* Draw a resize box.
* @param r Rectangle of the box.
* @param colour Colour of the resize box.
* @param at_left Resize box is at left-side of the window,
* @param clicked Box is lowered.
*/
static inline void DrawResizeBox(const Rect &r, Colours colour, bool at_left, bool clicked)
{
DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE);
if (at_left) {
Dimension d = GetSpriteSize(SPR_WINDOW_RESIZE_LEFT);
DrawSprite(SPR_WINDOW_RESIZE_LEFT, PAL_NONE, r.left + WD_RESIZEBOX_RIGHT + clicked,
r.bottom + 1 - WD_RESIZEBOX_BOTTOM - d.height + clicked);
} else {
Dimension d = GetSpriteSize(SPR_WINDOW_RESIZE_RIGHT);
DrawSprite(SPR_WINDOW_RESIZE_RIGHT, PAL_NONE, r.right + 1 - WD_RESIZEBOX_RIGHT - d.width + clicked,
r.bottom + 1 - WD_RESIZEBOX_BOTTOM - d.height + clicked);
}
}
/**
* Draw a close box.
* @param r Rectangle of the box.
* @param colour Colour of the close box.
*/
static inline void DrawCloseBox(const Rect &r, Colours colour)
{
if (colour != COLOUR_WHITE) DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, FR_NONE);
Dimension d = GetSpriteSize(SPR_CLOSEBOX);
int s = UnScaleGUI(1); /* Offset to account for shadow of SPR_CLOSEBOX */
DrawSprite(SPR_CLOSEBOX, (colour != COLOUR_WHITE ? TC_BLACK : TC_SILVER) | (1 << PALETTE_TEXT_RECOLOUR), CenterBounds(r.left, r.right, d.width - s), CenterBounds(r.top, r.bottom, d.height - s));
}
/**
* Draw a caption bar.
* @param r Rectangle of the bar.
* @param colour Colour of the window.
* @param owner 'Owner' of the window.
* @param str Text to draw in the bar.
*/
void DrawCaption(const Rect &r, Colours colour, Owner owner, StringID str)
{
bool company_owned = owner < MAX_COMPANIES;
DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, FR_BORDERONLY);
DrawFrameRect(r.left + 1, r.top + 1, r.right - 1, r.bottom - 1, colour, company_owned ? FR_LOWERED | FR_DARKENED | FR_BORDERONLY : FR_LOWERED | FR_DARKENED);
if (company_owned) {
GfxFillRect(r.left + 2, r.top + 2, r.right - 2, r.bottom - 2, _colour_gradient[_company_colours[owner]][4]);
}
if (str != STR_NULL) {
Dimension d = GetStringBoundingBox(str);
int offset = max(0, ((int)(r.bottom - r.top + 1) - (int)d.height) / 2); // Offset for rendering the text vertically centered
DrawString(r.left + WD_CAPTIONTEXT_LEFT, r.right - WD_CAPTIONTEXT_RIGHT, r.top + offset, str, TC_FROMSTRING, SA_HOR_CENTER);
}
}
/**
* Draw a button with a dropdown (#WWT_DROPDOWN and #NWID_BUTTON_DROPDOWN).
* @param r Rectangle containing the widget.
* @param colour Background colour of the widget.
* @param clicked_button The button-part is lowered.
* @param clicked_dropdown The drop-down part is lowered.
* @param str Text of the button.
*
* @note Magic constants are also used in #NWidgetLeaf::ButtonHit.
*/
static inline void DrawButtonDropdown(const Rect &r, Colours colour, bool clicked_button, bool clicked_dropdown, StringID str)
{
int text_offset = max(0, ((int)(r.bottom - r.top + 1) - FONT_HEIGHT_NORMAL) / 2); // Offset for rendering the text vertically centered
int dd_width = NWidgetLeaf::dropdown_dimension.width;
int dd_height = NWidgetLeaf::dropdown_dimension.height;
int image_offset = max(0, ((int)(r.bottom - r.top + 1) - dd_height) / 2);
if (_current_text_dir == TD_LTR) {
DrawFrameRect(r.left, r.top, r.right - dd_width, r.bottom, colour, clicked_button ? FR_LOWERED : FR_NONE);
DrawFrameRect(r.right + 1 - dd_width, r.top, r.right, r.bottom, colour, clicked_dropdown ? FR_LOWERED : FR_NONE);
DrawSprite(SPR_ARROW_DOWN, PAL_NONE, r.right - (dd_width - 2) + clicked_dropdown, r.top + image_offset + clicked_dropdown);
if (str != STR_NULL) DrawString(r.left + WD_DROPDOWNTEXT_LEFT + clicked_button, r.right - dd_width - WD_DROPDOWNTEXT_RIGHT + clicked_button, r.top + text_offset + clicked_button, str, TC_BLACK);
} else {
DrawFrameRect(r.left + dd_width, r.top, r.right, r.bottom, colour, clicked_button ? FR_LOWERED : FR_NONE);
DrawFrameRect(r.left, r.top, r.left + dd_width - 1, r.bottom, colour, clicked_dropdown ? FR_LOWERED : FR_NONE);
DrawSprite(SPR_ARROW_DOWN, PAL_NONE, r.left + 1 + clicked_dropdown, r.top + image_offset + clicked_dropdown);
if (str != STR_NULL) DrawString(r.left + dd_width + WD_DROPDOWNTEXT_LEFT + clicked_button, r.right - WD_DROPDOWNTEXT_RIGHT + clicked_button, r.top + text_offset + clicked_button, str, TC_BLACK);
}
}
/**
* Draw a dropdown #WWT_DROPDOWN widget.
* @param r Rectangle containing the widget.
* @param colour Background colour of the widget.
* @param clicked The widget is lowered.
* @param str Text of the button.
*/
static inline void DrawDropdown(const Rect &r, Colours colour, bool clicked, StringID str)
{
DrawButtonDropdown(r, colour, false, clicked, str);
}
/**
* Paint all widgets of a window.
*/
void Window::DrawWidgets() const
{
this->nested_root->Draw(this);
if (this->flags & WF_WHITE_BORDER) {
DrawFrameRect(0, 0, this->width - 1, this->height - 1, COLOUR_WHITE, FR_BORDERONLY);
}
if (this->flags & WF_HIGHLIGHTED) {
extern bool _window_highlight_colour;
for (uint i = 0; i < this->nested_array_size; i++) {
const NWidgetBase *widget = this->GetWidget<NWidgetBase>(i);
if (widget == nullptr || !widget->IsHighlighted()) continue;
int left = widget->pos_x;
int top = widget->pos_y;
int right = left + widget->current_x - 1;
int bottom = top + widget->current_y - 1;
int colour = _string_colourmap[_window_highlight_colour ? widget->GetHighlightColour() : TC_WHITE];
GfxFillRect(left, top, left, bottom - WD_BEVEL_BOTTOM, colour);
GfxFillRect(left + WD_BEVEL_LEFT, top, right - WD_BEVEL_RIGHT, top, colour);
GfxFillRect(right, top, right, bottom - WD_BEVEL_BOTTOM, colour);
GfxFillRect(left, bottom, right, bottom, colour);
}
}
}
/**
* Draw a sort button's up or down arrow symbol.
* @param widget Sort button widget
* @param state State of sort button
*/
void Window::DrawSortButtonState(int widget, SortButtonState state) const
{
if (state == SBS_OFF) return;
assert(this->nested_array != nullptr);
const NWidgetBase *nwid = this->GetWidget<NWidgetBase>(widget);
/* Sort button uses the same sprites as vertical scrollbar */
Dimension dim = NWidgetScrollbar::GetVerticalDimension();
int offset = this->IsWidgetLowered(widget) ? 1 : 0;
int x = offset + nwid->pos_x + (_current_text_dir == TD_LTR ? nwid->current_x - dim.width : 0);
int y = offset + nwid->pos_y + (nwid->current_y - dim.height) / 2;
DrawSprite(state == SBS_DOWN ? SPR_ARROW_DOWN : SPR_ARROW_UP, PAL_NONE, x, y);
}
/**
* Get width of up/down arrow of sort button state.
* @return Width of space required by sort button arrow.
*/
int Window::SortButtonWidth()
{
return NWidgetScrollbar::GetVerticalDimension().width + 1;
}
/**
* @defgroup NestedWidgets Hierarchical widgets
* Hierarchical widgets, also known as nested widgets, are widgets stored in a tree. At the leafs of the tree are (mostly) the 'real' widgets
* visible to the user. At higher levels, widgets get organized in container widgets, until all widgets of the window are merged.
*
* \section nestedwidgetkinds Hierarchical widget kinds
* A leaf widget is one of
* <ul>
* <li> #NWidgetLeaf for widgets visible for the user, or
* <li> #NWidgetSpacer for creating (flexible) empty space between widgets.
* </ul>
* The purpose of a leaf widget is to provide interaction with the user by displaying settings, and/or allowing changing the settings.
*
* A container widget is one of
* <ul>
* <li> #NWidgetHorizontal for organizing child widgets in a (horizontal) row. The row switches order depending on the language setting (thus supporting
* right-to-left languages),
* <li> #NWidgetHorizontalLTR for organizing child widgets in a (horizontal) row, always in the same order. All children below this container will also
* never swap order.
* <li> #NWidgetVertical for organizing child widgets underneath each other.
* <li> #NWidgetMatrix for organizing child widgets in a matrix form.
* <li> #NWidgetBackground for adding a background behind its child widget.
* <li> #NWidgetStacked for stacking child widgets on top of each other.
* </ul>
* The purpose of a container widget is to structure its leafs and sub-containers to allow proper resizing.
*
* \section nestedwidgetscomputations Hierarchical widget computations
* The first 'computation' is the creation of the nested widgets tree by calling the constructors of the widgets listed above and calling \c Add() for every child,
* or by means of specifying the tree as a collection of nested widgets parts and instantiating the tree from the array.
*
* After the creation step,
* - The leafs have their own minimal size (\e min_x, \e min_y), filling (\e fill_x, \e fill_y), and resize steps (\e resize_x, \e resize_y).
* - Containers only know what their children are, \e fill_x, \e fill_y, \e resize_x, and \e resize_y are not initialized.
*
* Computations in the nested widgets take place as follows:
* <ol>
* <li> A bottom-up sweep by recursively calling NWidgetBase::SetupSmallestSize() to initialize the smallest size (\e smallest_x, \e smallest_y) and
* to propagate filling and resize steps upwards to the root of the tree.
* <li> A top-down sweep by recursively calling NWidgetBase::AssignSizePosition() with #ST_SMALLEST to make the smallest sizes consistent over
* the entire tree, and to assign the top-left (\e pos_x, \e pos_y) position of each widget in the tree. This step uses \e fill_x and \e fill_y at each
* node in the tree to decide how to fill each widget towards consistent sizes. Also the current size (\e current_x and \e current_y) is set.
* <li> After initializing the smallest size in the widget tree with #ST_SMALLEST, the tree can be resized (the current size modified) by calling
* NWidgetBase::AssignSizePosition() at the root with #ST_RESIZE and the new size of the window. For proper functioning, the new size should be the smallest
* size + a whole number of resize steps in both directions (ie you can only resize in steps of length resize_{x,y} from smallest_{x,y}).
* </ol>
* After the second step, the current size of the widgets are set to the smallest size.
*
* To resize, perform the last step with the new window size. This can be done as often as desired.
* When the smallest size of at least one widget changes, the whole procedure has to be redone from the start.
*
* @see NestedWidgetParts
*/
/**
* Base class constructor.
* @param tp Nested widget type.
*/
NWidgetBase::NWidgetBase(WidgetType tp) : ZeroedMemoryAllocator()
{
this->type = tp;
}
/* ~NWidgetContainer() takes care of #next and #prev data members. */
/**
* @fn void NWidgetBase::SetupSmallestSize(Window *w, bool init_array)
* Compute smallest size needed by the widget.
*
* The smallest size of a widget is the smallest size that a widget needs to
* display itself properly. In addition, filling and resizing of the widget are computed.
* The function calls #Window::UpdateWidgetSize for each leaf widget and
* background widget without child with a non-negative index.
*
* @param w Window owning the widget.
* @param init_array Initialize the \c w->nested_array.
*
* @note After the computation, the results can be queried by accessing the #smallest_x and #smallest_y data members of the widget.
*/
/**
* @fn void NWidgetBase::AssignSizePosition(SizingType sizing, uint x, uint y, uint given_width, uint given_height, bool rtl)
* Assign size and position to the widget.
* @param sizing Type of resizing to perform.
* @param x Horizontal offset of the widget relative to the left edge of the window.
* @param y Vertical offset of the widget relative to the top edge of the window.
* @param given_width Width allocated to the widget.
* @param given_height Height allocated to the widget.
* @param rtl Adapt for right-to-left languages (position contents of horizontal containers backwards).
*
* Afterwards, \e pos_x and \e pos_y contain the top-left position of the widget, \e smallest_x and \e smallest_y contain
* the smallest size such that all widgets of the window are consistent, and \e current_x and \e current_y contain the current size.
*/
/**
* @fn void NWidgetBase::FillNestedArray(NWidgetBase **array, uint length)
* Fill the Window::nested_array array with pointers to nested widgets in the tree.
* @param array Base pointer of the array.
* @param length Length of the array.
*/
/**
* @fn void NWidgetBase::Draw(const Window *w)
* Draw the widgets of the tree.
* The function calls #Window::DrawWidget for each widget with a non-negative index, after the widget itself is painted.
* @param w Window that owns the tree.
*/
/**
* Mark the widget as 'dirty' (in need of repaint).
* @param w Window owning the widget.
*/
void NWidgetBase::SetDirty(const Window *w) const
{
int abs_left = w->left + this->pos_x;
int abs_top = w->top + this->pos_y;
AddDirtyBlock(abs_left, abs_top, abs_left + this->current_x, abs_top + this->current_y);
}
/**
* @fn NWidgetCore *NWidgetBase::GetWidgetFromPos(int x, int y)
* Retrieve a widget by its position.
* @param x Horizontal position relative to the left edge of the window.
* @param y Vertical position relative to the top edge of the window.
* @return Returns the deepest nested widget that covers the given position, or \c nullptr if no widget can be found.
*/
/**
* Retrieve a widget by its type.
* @param tp Widget type to search for.
* @return Returns the first widget of the specified type, or \c nullptr if no widget can be found.
*/
NWidgetBase *NWidgetBase::GetWidgetOfType(WidgetType tp)
{
return (this->type == tp) ? this : nullptr;
}
/**
* Constructor for resizable nested widgets.
* @param tp Nested widget type.
* @param fill_x Horizontal fill step size, \c 0 means no filling is allowed.
* @param fill_y Vertical fill step size, \c 0 means no filling is allowed.
*/
NWidgetResizeBase::NWidgetResizeBase(WidgetType tp, uint fill_x, uint fill_y) : NWidgetBase(tp)
{
this->fill_x = fill_x;
this->fill_y = fill_y;
}
/**
* Set minimal size of the widget.
* @param min_x Horizontal minimal size of the widget.
* @param min_y Vertical minimal size of the widget.
*/
void NWidgetResizeBase::SetMinimalSize(uint min_x, uint min_y)
{
this->min_x = max(this->min_x, min_x);
this->min_y = max(this->min_y, min_y);
}
/**
* Set minimal text lines for the widget.
* @param min_lines Number of text lines of the widget.
* @param spacing Extra spacing (eg WD_FRAMERECT_TOP + _BOTTOM) of the widget.
* @param size Font size of text.
*/
void NWidgetResizeBase::SetMinimalTextLines(uint8 min_lines, uint8 spacing, FontSize size)
{
this->min_y = min_lines * GetCharacterHeight(size) + spacing;
}
/**
* Set the filling of the widget from initial size.
* @param fill_x Horizontal fill step size, \c 0 means no filling is allowed.
* @param fill_y Vertical fill step size, \c 0 means no filling is allowed.
*/
void NWidgetResizeBase::SetFill(uint fill_x, uint fill_y)
{
this->fill_x = fill_x;
this->fill_y = fill_y;
}
/**
* Set resize step of the widget.
* @param resize_x Resize step in horizontal direction, value \c 0 means no resize, otherwise the step size in pixels.
* @param resize_y Resize step in vertical direction, value \c 0 means no resize, otherwise the step size in pixels.
*/
void NWidgetResizeBase::SetResize(uint resize_x, uint resize_y)
{
this->resize_x = resize_x;
this->resize_y = resize_y;
}
void NWidgetResizeBase::AssignSizePosition(SizingType sizing, uint x, uint y, uint given_width, uint given_height, bool rtl)
{
this->StoreSizePosition(sizing, x, y, given_width, given_height);
}
/**
* Initialization of a 'real' widget.
* @param tp Type of the widget.
* @param colour Colour of the widget.
* @param fill_x Default horizontal filling.
* @param fill_y Default vertical filling.
* @param widget_data Data component of the widget. @see Widget::data
* @param tool_tip Tool tip of the widget. @see Widget::tooltips
*/
NWidgetCore::NWidgetCore(WidgetType tp, Colours colour, uint fill_x, uint fill_y, uint32 widget_data, StringID tool_tip) : NWidgetResizeBase(tp, fill_x, fill_y)
{
this->colour = colour;
this->index = -1;
this->widget_data = widget_data;
this->tool_tip = tool_tip;
this->scrollbar_index = -1;
}
/**
* Set index of the nested widget in the widget array.
* @param index Index to use.
*/
void NWidgetCore::SetIndex(int index)
{
assert(index >= 0);
this->index = index;
}
/**
* Set data and tool tip of the nested widget.
* @param widget_data Data to use.
* @param tool_tip Tool tip string to use.
*/
void NWidgetCore::SetDataTip(uint32 widget_data, StringID tool_tip)
{
this->widget_data = widget_data;
this->tool_tip = tool_tip;
}
void NWidgetCore::FillNestedArray(NWidgetBase **array, uint length)
{
if (this->index >= 0 && (uint)(this->index) < length) array[this->index] = this;
}
NWidgetCore *NWidgetCore::GetWidgetFromPos(int x, int y)
{
return (IsInsideBS(x, this->pos_x, this->current_x) && IsInsideBS(y, this->pos_y, this->current_y)) ? this : nullptr;
}
/**
* Constructor container baseclass.
* @param tp Type of the container.
*/
NWidgetContainer::NWidgetContainer(WidgetType tp) : NWidgetBase(tp)
{
this->head = nullptr;
this->tail = nullptr;
}
NWidgetContainer::~NWidgetContainer()
{
while (this->head != nullptr) {
NWidgetBase *wid = this->head->next;
delete this->head;
this->head = wid;
}
this->tail = nullptr;
}
NWidgetBase *NWidgetContainer::GetWidgetOfType(WidgetType tp)
{
if (this->type == tp) return this;
for (NWidgetBase *child_wid = this->head; child_wid != nullptr; child_wid = child_wid->next) {
NWidgetBase *nwid = child_wid->GetWidgetOfType(tp);
if (nwid != nullptr) return nwid;
}
return nullptr;
}
/**
* Append widget \a wid to container.
* @param wid Widget to append.
*/
void NWidgetContainer::Add(NWidgetBase *wid)
{
assert(wid->next == nullptr && wid->prev == nullptr);
if (this->head == nullptr) {
this->head = wid;
this->tail = wid;
} else {
assert(this->tail != nullptr);
assert(this->tail->next == nullptr);
this->tail->next = wid;
wid->prev = this->tail;
this->tail = wid;
}
}
void NWidgetContainer::FillNestedArray(NWidgetBase **array, uint length)
{
for (NWidgetBase *child_wid = this->head; child_wid != nullptr; child_wid = child_wid->next) {
child_wid->FillNestedArray(array, length);
}
}
/**
* Widgets stacked on top of each other.
*/
NWidgetStacked::NWidgetStacked() : NWidgetContainer(NWID_SELECTION)
{
this->index = -1;
}
void NWidgetStacked::SetIndex(int index)
{
this->index = index;
}
void NWidgetStacked::SetupSmallestSize(Window *w, bool init_array)
{
if (this->index >= 0 && init_array) { // Fill w->nested_array[]
assert(w->nested_array_size > (uint)this->index);
w->nested_array[this->index] = this;
}
/* Zero size plane selected */
if (this->shown_plane >= SZSP_BEGIN) {
Dimension size = {0, 0};
Dimension padding = {0, 0};
Dimension fill = {(this->shown_plane == SZSP_HORIZONTAL), (this->shown_plane == SZSP_VERTICAL)};
Dimension resize = {(this->shown_plane == SZSP_HORIZONTAL), (this->shown_plane == SZSP_VERTICAL)};
/* Here we're primarily interested in the value of resize */
if (this->index >= 0) w->UpdateWidgetSize(this->index, &size, padding, &fill, &resize);
this->smallest_x = size.width;
this->smallest_y = size.height;
this->fill_x = fill.width;
this->fill_y = fill.height;
this->resize_x = resize.width;
this->resize_y = resize.height;