Completed
Pull Request — master (#203)
by
unknown
01:54
created

MenuStyle::calculateContentWidth()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 2
b 0
f 0
nc 2
nop 0
dl 0
loc 8
rs 10
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Terminal\TerminalFactory;
6
use PhpSchool\CliMenu\Util\ColourUtil;
7
use PhpSchool\Terminal\Terminal;
8
use Assert\Assertion;
9
10
//TODO: B/W fallback
11
12
/**
13
 * @author Michael Woodward <[email protected]>
14
 */
15
class MenuStyle
16
{
17
    /**
18
     * @var Terminal
19
     */
20
    protected $terminal;
21
22
    /**
23
     * @var string
24
     */
25
    protected $fg;
26
27
    /**
28
     * @var string
29
     */
30
    protected $bg;
31
32
    /**
33
     * @var int
34
     */
35
    protected $width;
36
37
    /**
38
     * @var int
39
     */
40
    protected $margin;
41
42
    /**
43
     * @var int
44
     */
45
    protected $paddingTopBottom;
46
47
    /**
48
     * @var int
49
     */
50
    protected $paddingLeftRight;
51
52
    /**
53
     * @var array
54
     */
55
    private $paddingTopBottomRows = [];
56
57
    /**
58
     * @var int
59
     */
60
    protected $contentWidth;
61
62
    /**
63
     * @var string
64
     */
65
    private $selectedMarker;
66
67
    /**
68
     * @var string
69
     */
70
    private $unselectedMarker;
71
72
    /**
73
     * @var string
74
     */
75
    private $itemExtra;
76
77
    /**
78
     * @var bool
79
     */
80
    private $displaysExtra;
81
82
    /**
83
     * @var string
84
     */
85
    private $titleSeparator;
86
87
    /**
88
     * @var string
89
     */
90
    private $coloursSetCode;
91
92
    /**
93
     * @var int
94
     */
95
    private $borderTopWidth;
96
97
    /**
98
     * @var int
99
     */
100
    private $borderRightWidth;
101
102
    /**
103
     * @var int
104
     */
105
    private $borderBottomWidth;
106
107
    /**
108
     * @var int
109
     */
110
    private $borderLeftWidth;
111
112
    /**
113
     * @var string
114
     */
115
    private $borderColour = 'white';
116
117
    /**
118
     * @var array
119
     */
120
    private $borderTopRows = [];
121
122
    /**
123
     * @var array
124
     */
125
    private $borderBottomRows = [];
126
127
    /**
128
     * @var bool
129
     */
130
    private $marginAuto = false;
131
132
    /**
133
     * Default Values
134
     *
135
     * @var array
136
     */
137
    private static $defaultStyleValues = [
138
        'fg' => 'white',
139
        'bg' => 'blue',
140
        'width' => 100,
141
        'paddingTopBottom' => 1,
142
        'paddingLeftRight' => 2,
143
        'margin' => 2,
144
        'selectedMarker' => '● ',
145
        'unselectedMarker' => '○ ',
146
        'itemExtra' => '✔',
147
        'displaysExtra' => false,
148
        'titleSeparator' => '=',
149
        'borderTopWidth' => 0,
150
        'borderRightWidth' => 0,
151
        'borderBottomWidth' => 0,
152
        'borderLeftWidth' => 0,
153
        'borderColour' => 'white',
154
        'marginAuto' => false,
155
    ];
156
157
    /**
158
     * @var array
159
     */
160
    private static $availableOptions = [
161
        'bold'       => ['set' => 1, 'unset' => 22],
162
        'dim'        => ['set' => 2, 'unset' => 22],
163
        'underscore' => ['set' => 4, 'unset' => 24],
164
        'blink'      => ['set' => 5, 'unset' => 25],
165
        'reverse'    => ['set' => 7, 'unset' => 27],
166
        'conceal'    => ['set' => 8, 'unset' => 28]
167
    ];
168
169
    /**
170
     * Initialise style
171
     */
172
    public function __construct(Terminal $terminal = null)
173
    {
174
        $this->terminal = $terminal ?: TerminalFactory::fromSystem();
175
176
        $this->fg = self::$defaultStyleValues['fg'];
177
        $this->bg = self::$defaultStyleValues['bg'];
178
179
        $this->generateColoursSetCode();
180
181
        $this->setWidth(self::$defaultStyleValues['width']);
182
        $this->setPaddingTopBottom(self::$defaultStyleValues['paddingTopBottom']);
183
        $this->setPaddingLeftRight(self::$defaultStyleValues['paddingLeftRight']);
184
        $this->setMargin(self::$defaultStyleValues['margin']);
185
        $this->setSelectedMarker(self::$defaultStyleValues['selectedMarker']);
186
        $this->setUnselectedMarker(self::$defaultStyleValues['unselectedMarker']);
187
        $this->setItemExtra(self::$defaultStyleValues['itemExtra']);
188
        $this->setDisplaysExtra(self::$defaultStyleValues['displaysExtra']);
189
        $this->setTitleSeparator(self::$defaultStyleValues['titleSeparator']);
190
        $this->setBorderTopWidth(self::$defaultStyleValues['borderTopWidth']);
191
        $this->setBorderRightWidth(self::$defaultStyleValues['borderRightWidth']);
192
        $this->setBorderBottomWidth(self::$defaultStyleValues['borderBottomWidth']);
193
        $this->setBorderLeftWidth(self::$defaultStyleValues['borderLeftWidth']);
194
        $this->setBorderColour(self::$defaultStyleValues['borderColour']);
195
    }
196
197
    public function hasChangedFromDefaults() : bool
198
    {
199
        $currentValues = [
200
            $this->fg,
201
            $this->bg,
202
            $this->width,
203
            $this->paddingTopBottom,
204
            $this->paddingLeftRight,
205
            $this->margin,
206
            $this->selectedMarker,
207
            $this->unselectedMarker,
208
            $this->itemExtra,
209
            $this->displaysExtra,
210
            $this->titleSeparator,
211
            $this->borderTopWidth,
212
            $this->borderRightWidth,
213
            $this->borderBottomWidth,
214
            $this->borderLeftWidth,
215
            $this->borderColour,
216
            $this->marginAuto,
217
        ];
218
                
219
        return $currentValues !== array_values(self::$defaultStyleValues);
220
    }
221
222
    public function getDisabledItemText(string $text) : string
223
    {
224
        return sprintf(
225
            "\033[%sm%s\033[%sm",
226
            self::$availableOptions['dim']['set'],
227
            $text,
228
            self::$availableOptions['dim']['unset']
229
        );
230
    }
231
232
    /**
233
     * Generates the ansi escape sequence to set the colours
234
     */
235
    private function generateColoursSetCode() : void
236
    {
237
        if (!ctype_digit($this->fg)) {
238
            $fgCode = Style\Colour::AVAILABLE_FOREGROUND_COLOURS[$this->fg];
239
        } else {
240
            $fgCode = sprintf("38;5;%s", $this->fg);
241
        }
242
243
        if (!ctype_digit($this->bg)) {
244
            $bgCode = Style\Colour::AVAILABLE_BACKGROUND_COLOURS[$this->bg];
245
        } else {
246
            $bgCode = sprintf("48;5;%s", $this->bg);
247
        }
248
249
        $this->coloursSetCode = sprintf("\033[%s;%sm", $fgCode, $bgCode);
250
    }
251
252
    /**
253
     * Get the colour code for Bg and Fg
254
     */
255
    public function getColoursSetCode() : string
256
    {
257
        return $this->coloursSetCode;
258
    }
259
260
    /**
261
     * Get the inverted escape sequence (used for selected elements)
262
     */
263
    public function getInvertedColoursSetCode() : string
264
    {
265
        return Style\Colour::INVERTED_SET_CODE;
266
    }
267
268
    /**
269
     * Get the inverted escape sequence (used for selected elements)
270
     */
271
    public function getInvertedColoursUnsetCode() : string
272
    {
273
        return Style\Colour::INVERTED_UNSET_CODE;
274
    }
275
276
    /**
277
     * Get the escape sequence used to reset colours to default
278
     */
279
    public function getColoursResetCode() : string
280
    {
281
        return Style\Colour::RESET_CODE;
282
    }
283
284
    /**
285
     * Calculate the contents width
286
     */
287
    protected function calculateContentWidth() : void
288
    {
289
        $this->contentWidth = $this->width
290
            - ($this->paddingLeftRight * 2)
291
            - ($this->borderRightWidth + $this->borderLeftWidth);
292
293
        if ($this->contentWidth < 0) {
294
            $this->contentWidth = 0;
295
        }
296
    }
297
298
    public function getFg()
299
    {
300
        return $this->fg;
301
    }
302
303
    public function setFg(string $fg, string $fallback = null) : self
304
    {
305
        $this->fg = ColourUtil::validateColour(
306
            $this->terminal,
307
            $fg,
308
            $fallback
309
        );
310
        $this->generateColoursSetCode();
311
312
        return $this;
313
    }
314
315
    public function getBg()
316
    {
317
        return $this->bg;
318
    }
319
320
    public function setBg(string $bg, string $fallback = null) : self
321
    {
322
        $this->bg = ColourUtil::validateColour(
323
            $this->terminal,
324
            $bg,
325
            $fallback
326
        );
327
328
        $this->generateColoursSetCode();
329
        $this->generatePaddingTopBottomRows();
330
331
        return $this;
332
    }
333
334
    public function getWidth() : int
335
    {
336
        return $this->width;
337
    }
338
339
    public function setWidth(int $width) : self
340
    {
341
        Assertion::greaterOrEqualThan($width, 0);
342
343
        if ($width >= $this->terminal->getWidth()) {
344
            $width = $this->terminal->getWidth();
345
        }
346
347
        $this->width = $width;
348
        if ($this->marginAuto) {
349
            $this->setMarginAuto();
350
        }
351
352
        $this->calculateContentWidth();
353
        $this->generateBorderRows();
354
        $this->generatePaddingTopBottomRows();
355
356
        return $this;
357
    }
358
359
    public function getPaddingTopBottom() : int
360
    {
361
        return $this->paddingTopBottom;
362
    }
363
364
    public function getPaddingLeftRight() : int
365
    {
366
        return $this->paddingLeftRight;
367
    }
368
369
    private function generatePaddingTopBottomRows() : void
370
    {
371
        if ($this->borderLeftWidth || $this->borderRightWidth) {
372
            $borderColour = $this->getBorderColourCode();
373
        } else {
374
            $borderColour = '';
375
        }
376
377
        $paddingRow = sprintf(
378
            "%s%s%s%s%s%s%s%s%s%s\n",
379
            str_repeat(' ', $this->margin),
380
            $borderColour,
381
            str_repeat(' ', $this->borderLeftWidth),
382
            $this->getColoursSetCode(),
383
            str_repeat(' ', $this->paddingLeftRight),
384
            str_repeat(' ', $this->contentWidth),
385
            str_repeat(' ', $this->paddingLeftRight),
386
            $borderColour,
387
            str_repeat(' ', $this->borderRightWidth),
388
            Style\Colour::RESET_CODE
389
        );
390
391
        $this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
392
    }
393
394
    public function getPaddingTopBottomRows() : array
395
    {
396
        return $this->paddingTopBottomRows;
397
    }
398
399
    public function setPadding(int $topBottom, int $leftRight = null) : self
400
    {
401
        if ($leftRight === null) {
402
            $leftRight = $topBottom;
403
        }
404
405
        $this->setPaddingTopBottom($topBottom);
406
        $this->setPaddingLeftRight($leftRight);
407
408
        $this->calculateContentWidth();
409
        $this->generatePaddingTopBottomRows();
410
411
        return $this;
412
    }
413
414
    public function setPaddingTopBottom(int $topBottom) : self
415
    {
416
        Assertion::greaterOrEqualThan($topBottom, 0);
417
        $this->paddingTopBottom = $topBottom;
418
419
        $this->generatePaddingTopBottomRows();
420
421
        return $this;
422
    }
423
424
    public function setPaddingLeftRight(int $leftRight) : self
425
    {
426
        Assertion::greaterOrEqualThan($leftRight, 0);
427
        $this->paddingLeftRight = $leftRight;
428
429
        $this->calculateContentWidth();
430
        $this->generatePaddingTopBottomRows();
431
432
        return $this;
433
    }
434
435
    public function getMargin() : int
436
    {
437
        return $this->margin;
438
    }
439
440
    public function setMarginAuto() : self
441
    {
442
        $this->marginAuto = true;
443
        $this->margin = (int) floor(($this->terminal->getWidth() - $this->width) / 2);
444
445
        $this->generateBorderRows();
446
        $this->generatePaddingTopBottomRows();
447
448
        return $this;
449
    }
450
451
    public function setMargin(int $margin) : self
452
    {
453
        Assertion::greaterOrEqualThan($margin, 0);
454
455
        $this->marginAuto = false;
456
        $this->margin = $margin;
457
458
        $this->generateBorderRows();
459
        $this->generatePaddingTopBottomRows();
460
461
        return $this;
462
    }
463
464
    public function getContentWidth() : int
465
    {
466
        return $this->contentWidth;
467
    }
468
469
    /**
470
     * Get padding for right had side of content
471
     */
472
    public function getRightHandPadding(int $contentLength) : int
473
    {
474
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();
475
476
        if ($rightPadding < 0) {
477
            $rightPadding = 0;
478
        }
479
480
        return $rightPadding;
481
    }
482
483
    public function getSelectedMarker() : string
484
    {
485
        return $this->selectedMarker;
486
    }
487
488
    public function setSelectedMarker(string $marker) : self
489
    {
490
        $this->selectedMarker = $marker;
491
492
        return $this;
493
    }
494
495
    public function getUnselectedMarker() : string
496
    {
497
        return $this->unselectedMarker;
498
    }
499
500
    public function setUnselectedMarker(string $marker) : self
501
    {
502
        $this->unselectedMarker = $marker;
503
504
        return $this;
505
    }
506
507
    /**
508
     * Get the correct marker for the item
509
     */
510
    public function getMarker(bool $selected) : string
511
    {
512
        return $selected ? $this->selectedMarker : $this->unselectedMarker;
513
    }
514
515
    public function setItemExtra(string $itemExtra) : self
516
    {
517
        $this->itemExtra = $itemExtra;
518
519
        return $this;
520
    }
521
522
    public function getItemExtra() : string
523
    {
524
        return $this->itemExtra;
525
    }
526
527
    public function getDisplaysExtra() : bool
528
    {
529
        return $this->displaysExtra;
530
    }
531
532
    public function setDisplaysExtra(bool $displaysExtra) : self
533
    {
534
        $this->displaysExtra = $displaysExtra;
535
536
        return $this;
537
    }
538
539
    public function getTitleSeparator() : string
540
    {
541
        return $this->titleSeparator;
542
    }
543
544
    public function setTitleSeparator(string $actionSeparator) : self
545
    {
546
        $this->titleSeparator = $actionSeparator;
547
548
        return $this;
549
    }
550
551
    private function generateBorderRows() : void
552
    {
553
        $borderRow = sprintf(
554
            "%s%s%s%s\n",
555
            str_repeat(' ', $this->margin),
556
            $this->getBorderColourCode(),
557
            str_repeat(' ', $this->width),
558
            Style\Colour::RESET_CODE
559
        );
560
561
        $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
562
        $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
563
    }
564
565
    public function getBorderTopRows() : array
566
    {
567
        return $this->borderTopRows;
568
    }
569
570
    public function getBorderBottomRows() : array
571
    {
572
        return $this->borderBottomRows;
573
    }
574
575
    /**
576
     * Shorthand function to set all borders values at once
577
     */
578
    public function setBorder(
579
        int $topWidth,
580
        $rightWidth = null,
581
        $bottomWidth = null,
582
        $leftWidth = null,
583
        string $colour = null
584
    ) : self {
585
        if (!is_int($rightWidth)) {
586
            $colour = $rightWidth;
587
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
588
        } elseif (!is_int($bottomWidth)) {
589
            $colour = $bottomWidth;
590
            $bottomWidth = $topWidth;
591
            $leftWidth = $rightWidth;
592
        } elseif (!is_int($leftWidth)) {
593
            $colour = $leftWidth;
594
            $leftWidth = $rightWidth;
595
        }
596
597
        $this->borderTopWidth = $topWidth;
598
        $this->borderRightWidth = $rightWidth;
599
        $this->borderBottomWidth = $bottomWidth;
600
        $this->borderLeftWidth = $leftWidth;
601
602
        if (is_string($colour)) {
603
            $this->setBorderColour($colour);
604
        } elseif ($colour !== null) {
605
            throw new \InvalidArgumentException('Invalid colour');
606
        }
607
608
        $this->calculateContentWidth();
609
        $this->generateBorderRows();
610
        $this->generatePaddingTopBottomRows();
611
612
        return $this;
613
    }
614
615
    public function setBorderTopWidth(int $width) : self
616
    {
617
        $this->borderTopWidth = $width;
618
619
        $this->generateBorderRows();
620
621
        return $this;
622
    }
623
624
    public function setBorderRightWidth(int $width) : self
625
    {
626
        $this->borderRightWidth = $width;
627
        $this->calculateContentWidth();
628
629
        $this->generatePaddingTopBottomRows();
630
631
        return $this;
632
    }
633
634
    public function setBorderBottomWidth(int $width) : self
635
    {
636
        $this->borderBottomWidth = $width;
637
638
        $this->generateBorderRows();
639
640
        return $this;
641
    }
642
643
    public function setBorderLeftWidth(int $width) : self
644
    {
645
        $this->borderLeftWidth = $width;
646
        $this->calculateContentWidth();
647
648
        $this->generatePaddingTopBottomRows();
649
650
        return $this;
651
    }
652
653
    public function setBorderColour(string $colour, $fallback = null) : self
654
    {
655
        $this->borderColour = ColourUtil::validateColour(
656
            $this->terminal,
657
            $colour,
658
            $fallback
659
        );
660
661
        $this->generateBorderRows();
662
        $this->generatePaddingTopBottomRows();
663
664
        return $this;
665
    }
666
667
    public function getBorderTopWidth() : int
668
    {
669
        return $this->borderTopWidth;
670
    }
671
672
    public function getBorderRightWidth() : int
673
    {
674
        return $this->borderRightWidth;
675
    }
676
677
    public function getBorderBottomWidth() : int
678
    {
679
        return $this->borderBottomWidth;
680
    }
681
682
    public function getBorderLeftWidth() : int
683
    {
684
        return $this->borderLeftWidth;
685
    }
686
687
    public function getBorderColour() : string
688
    {
689
        return $this->borderColour;
690
    }
691
692
    public function getBorderColourCode() : string
693
    {
694
        if (!ctype_digit($this->borderColour)) {
695
            $borderColourCode = Style\Colour::AVAILABLE_BACKGROUND_COLOURS[$this->borderColour];
696
        } else {
697
            $borderColourCode = sprintf("48;5;%s", $this->borderColour);
698
        }
699
700
        return sprintf("\033[%sm", $borderColourCode);
701
    }
702
}
703