diff --git a/examples/custom-styles.php b/examples/custom-styles.php index e06ffacd..dead0fb4 100644 --- a/examples/custom-styles.php +++ b/examples/custom-styles.php @@ -19,6 +19,7 @@ ->setForegroundColour('black') ->setPadding(4) ->setMargin(4) + ->setBorder(1, 2, 'red') ->setUnselectedMarker(' ') ->setSelectedMarker('>') ->setTitleSeparator('- ') diff --git a/src/CliMenu.php b/src/CliMenu.php index 0f02d346..420292ed 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -320,6 +320,10 @@ protected function draw() : void $frame->newLine(2); + if ($this->style->getBorderTopWidth() > 0) { + $frame->addRows($this->style->getBorderTopRows()); + } + if ($this->title) { $frame->addRows($this->drawMenuItem(new LineBreakItem())); $frame->addRows($this->drawMenuItem(new StaticItem($this->title))); @@ -331,6 +335,10 @@ protected function draw() : void }, $this->items, array_keys($this->items)); $frame->addRows($this->drawMenuItem(new LineBreakItem())); + + if ($this->style->getBorderBottomWidth() > 0) { + $frame->addRows($this->style->getBorderBottomRows()); + } $frame->newLine(2); @@ -359,15 +367,26 @@ protected function drawMenuItem(MenuItemInterface $item, bool $selected = false) ? $this->style->getInvertedColoursSetCode() : ''; - return array_map(function ($row) use ($setColour, $invertedColour, $resetColour) { + if ($this->style->getBorderLeftWidth() || $this->style->getBorderRightWidth()) { + $borderColour = $this->style->getBorderColourCode(); + } else { + $borderColour = ''; + } + + return array_map(function ($row) use ($setColour, $invertedColour, $resetColour, $borderColour) { return sprintf( - "%s%s%s%s%s%s%s%s\n", + "%s%s%s%s%s%s%s%s%s%s%s%s%s\n", str_repeat(' ', $this->style->getMargin()), + $borderColour, + str_repeat(' ', $this->style->getBorderLeftWidth()), $setColour, $invertedColour, str_repeat(' ', $this->style->getPadding()), $row, str_repeat(' ', $this->style->getRightHandPadding(mb_strlen(s::stripAnsiEscapeSequence($row)))), + $this->style->getInvertedColoursUnsetCode(), + $borderColour, + str_repeat(' ', $this->style->getBorderRightWidth()), $resetColour, str_repeat(' ', $this->style->getMargin()) ); diff --git a/src/CliMenuBuilder.php b/src/CliMenuBuilder.php index 07ef41be..4fd90371 100644 --- a/src/CliMenuBuilder.php +++ b/src/CliMenuBuilder.php @@ -280,6 +280,39 @@ public function setTitleSeparator(string $separator) : self return $this; } + public function setBorder( + int $topWidth, + $rightWidth = null, + $bottomWidth = null, + $leftWidth = null, + string $colour = null + ) : self { + if (!is_int($rightWidth)) { + $colour = $rightWidth; + $rightWidth = $bottomWidth = $leftWidth = $topWidth; + } elseif (!is_int($bottomWidth)) { + $colour = $bottomWidth; + $bottomWidth = $topWidth; + $leftWidth = $rightWidth; + } elseif (!is_int($leftWidth)) { + $colour = $leftWidth; + $leftWidth = $rightWidth; + } + + $this->style['borderTopWidth'] = $topWidth; + $this->style['borderRightWidth'] = $rightWidth; + $this->style['borderBottomWidth'] = $bottomWidth; + $this->style['borderLeftWidth'] = $leftWidth; + + if (is_string($colour)) { + $this->style['borderColour'] = $colour; + } elseif ($colour !== null) { + throw new \InvalidArgumentException('Invalid colour'); + } + + return $this; + } + public function setTerminal(Terminal $terminal) : self { $this->terminal = $terminal; @@ -344,7 +377,12 @@ private function buildStyle() : MenuStyle ->setUnselectedMarker($this->style['unselectedMarker']) ->setItemExtra($this->style['itemExtra']) ->setDisplaysExtra($this->style['displaysExtra']) - ->setTitleSeparator($this->style['titleSeparator']); + ->setTitleSeparator($this->style['titleSeparator']) + ->setBorderTopWidth($this->style['borderTopWidth']) + ->setBorderRightWidth($this->style['borderRightWidth']) + ->setBorderBottomWidth($this->style['borderBottomWidth']) + ->setBorderLeftWidth($this->style['borderLeftWidth']) + ->setBorderColour($this->style['borderColour']); $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']); diff --git a/src/MenuStyle.php b/src/MenuStyle.php index b59f4db0..2adee6a0 100644 --- a/src/MenuStyle.php +++ b/src/MenuStyle.php @@ -94,6 +94,41 @@ class MenuStyle */ private $coloursResetCode = "\033[0m"; + /** + * @var int + */ + private $borderTopWidth; + + /** + * @var int + */ + private $borderRightWidth; + + /** + * @var int + */ + private $borderBottomWidth; + + /** + * @var int + */ + private $borderLeftWidth; + + /** + * @var string + */ + private $borderColour = 'white'; + + /** + * @var array + */ + private $borderTopRows = []; + + /** + * @var array + */ + private $borderBottomRows = []; + /** * @var bool */ @@ -115,6 +150,11 @@ class MenuStyle 'itemExtra' => '✔', 'displaysExtra' => false, 'titleSeparator' => '=', + 'borderTopWidth' => 0, + 'borderRightWidth' => 0, + 'borderBottomWidth' => 0, + 'borderLeftWidth' => 0, + 'borderColour' => 'white', 'marginAuto' => false, ]; @@ -182,6 +222,11 @@ public function __construct(Terminal $terminal = null) $this->setItemExtra(static::$defaultStyleValues['itemExtra']); $this->setDisplaysExtra(static::$defaultStyleValues['displaysExtra']); $this->setTitleSeparator(static::$defaultStyleValues['titleSeparator']); + $this->setBorderTopWidth(static::$defaultStyleValues['borderTopWidth']); + $this->setBorderRightWidth(static::$defaultStyleValues['borderRightWidth']); + $this->setBorderBottomWidth(static::$defaultStyleValues['borderBottomWidth']); + $this->setBorderLeftWidth(static::$defaultStyleValues['borderLeftWidth']); + $this->setBorderColour(static::$defaultStyleValues['borderColour']); } public function getDisabledItemText(string $text) : string @@ -251,7 +296,9 @@ public function getColoursResetCode() : string */ protected function calculateContentWidth() : void { - $this->contentWidth = $this->width - ($this->padding * 2); + $this->contentWidth = $this->width + - ($this->padding * 2) + - ($this->borderRightWidth + $this->borderLeftWidth); } public function getFg() @@ -303,7 +350,9 @@ public function setWidth(int $width) : self if ($this->marginAuto) { $this->setMarginAuto(); } + $this->calculateContentWidth(); + $this->generateBorderRows(); return $this; } @@ -331,7 +380,9 @@ public function setMarginAuto() : self { $this->marginAuto = true; $this->margin = floor(($this->terminal->getWidth() - $this->width) / 2); - + + $this->generateBorderRows(); + return $this; } @@ -340,6 +391,8 @@ public function setMargin(int $margin) : self $this->marginAuto = false; $this->margin = $margin; + $this->generateBorderRows(); + return $this; } @@ -423,4 +476,151 @@ public function setTitleSeparator(string $actionSeparator) : self return $this; } + + private function generateBorderRows() : void + { + $borderRow = sprintf( + "%s%s%s%s%s\n", + str_repeat(' ', $this->margin), + $this->getBorderColourCode(), + str_repeat(' ', $this->width), + $this->coloursResetCode, + str_repeat(' ', $this->margin) + ); + + $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow); + $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow); + } + + public function getBorderTopRows() : array + { + return $this->borderTopRows; + } + + public function getBorderBottomRows() : array + { + return $this->borderBottomRows; + } + + /** + * Shorthand function to set all borders values at once + */ + public function setBorder( + int $topWidth, + $rightWidth = null, + $bottomWidth = null, + $leftWidth = null, + string $colour = null + ) : self { + if (!is_int($rightWidth)) { + $colour = $rightWidth; + $rightWidth = $bottomWidth = $leftWidth = $topWidth; + } elseif (!is_int($bottomWidth)) { + $colour = $bottomWidth; + $bottomWidth = $topWidth; + $leftWidth = $rightWidth; + } elseif (!is_int($leftWidth)) { + $colour = $leftWidth; + $leftWidth = $rightWidth; + } + + $this->borderTopWidth = $topWidth; + $this->borderRightWidth = $rightWidth; + $this->borderBottomWidth = $bottomWidth; + $this->borderLeftWidth = $leftWidth; + + if (is_string($colour)) { + $this->setBorderColour($colour); + } elseif ($colour !== null) { + throw new \InvalidArgumentException('Invalid colour'); + } + + $this->calculateContentWidth(); + $this->generateBorderRows(); + + return $this; + } + + public function setBorderTopWidth(int $width) : self + { + $this->borderTopWidth = $width; + + $this->generateBorderRows(); + + return $this; + } + + public function setBorderRightWidth(int $width) : self + { + $this->borderRightWidth = $width; + $this->calculateContentWidth(); + + return $this; + } + + public function setBorderBottomWidth(int $width) : self + { + $this->borderBottomWidth = $width; + + $this->generateBorderRows(); + + return $this; + } + + public function setBorderLeftWidth(int $width) : self + { + $this->borderLeftWidth = $width; + $this->calculateContentWidth(); + + return $this; + } + + public function setBorderColour(string $colour, $fallback = null) : self + { + $this->borderColour = ColourUtil::validateColour( + $this->terminal, + $colour, + $fallback + ); + + $this->generateBorderRows(); + + return $this; + } + + public function getBorderTopWidth() : int + { + return $this->borderTopWidth; + } + + public function getBorderRightWidth() : int + { + return $this->borderRightWidth; + } + + public function getBorderBottomWidth() : int + { + return $this->borderBottomWidth; + } + + public function getBorderLeftWidth() : int + { + return $this->borderLeftWidth; + } + + public function getBorderColour() : string + { + return $this->borderColour; + } + + public function getBorderColourCode() : string + { + if (!ctype_digit($this->borderColour)) { + $borderColourCode = self::$availableBackgroundColors[$this->borderColour]; + } else { + $borderColourCode = sprintf("48;5;%s", $this->borderColour); + } + + return sprintf("\033[%sm", $borderColourCode); + } } diff --git a/test/CliMenuBuilderTest.php b/test/CliMenuBuilderTest.php index 72f05dbd..328839e5 100644 --- a/test/CliMenuBuilderTest.php +++ b/test/CliMenuBuilderTest.php @@ -67,11 +67,11 @@ public function testModifyStyles() : void ->expects($this->any()) ->method('getWidth') ->will($this->returnValue(200)); - + $builder->setTerminal($terminal); - + $menu = $builder->build(); - + $this->checkStyleVariable($menu, 'bg', 'red'); $this->checkStyleVariable($menu, 'fg', 'red'); $this->checkStyleVariable($menu, 'width', 40); @@ -83,6 +83,95 @@ public function testModifyStyles() : void $this->checkStyleVariable($menu, 'titleSeparator', '-'); } + public function testSetBorderShorthandFunction() + { + $terminal = static::createMock(Terminal::class); + $terminal + ->expects($this->any()) + ->method('getWidth') + ->will($this->returnValue(200)); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2) + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 2); + $this->checkStyleVariable($menu, 'borderBottomWidth', 2); + $this->checkStyleVariable($menu, 'borderLeftWidth', 2); + $this->checkStyleVariable($menu, 'borderColour', 'white'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 4) + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 4); + $this->checkStyleVariable($menu, 'borderBottomWidth', 2); + $this->checkStyleVariable($menu, 'borderLeftWidth', 4); + $this->checkStyleVariable($menu, 'borderColour', 'white'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 4, 6) + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 4); + $this->checkStyleVariable($menu, 'borderBottomWidth', 6); + $this->checkStyleVariable($menu, 'borderLeftWidth', 4); + $this->checkStyleVariable($menu, 'borderColour', 'white'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 4, 6, 8) + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 4); + $this->checkStyleVariable($menu, 'borderBottomWidth', 6); + $this->checkStyleVariable($menu, 'borderLeftWidth', 8); + $this->checkStyleVariable($menu, 'borderColour', 'white'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 4, 6, 8, 'green') + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 4); + $this->checkStyleVariable($menu, 'borderBottomWidth', 6); + $this->checkStyleVariable($menu, 'borderLeftWidth', 8); + $this->checkStyleVariable($menu, 'borderColour', 'green'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 4, 6, 'green') + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 4); + $this->checkStyleVariable($menu, 'borderBottomWidth', 6); + $this->checkStyleVariable($menu, 'borderLeftWidth', 4); + $this->checkStyleVariable($menu, 'borderColour', 'green'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 4, 'green') + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 4); + $this->checkStyleVariable($menu, 'borderBottomWidth', 2); + $this->checkStyleVariable($menu, 'borderLeftWidth', 4); + $this->checkStyleVariable($menu, 'borderColour', 'green'); + + $menu = (new CliMenuBuilder) + ->setTerminal($terminal) + ->setBorder(2, 'green') + ->build(); + $this->checkStyleVariable($menu, 'borderTopWidth', 2); + $this->checkStyleVariable($menu, 'borderRightWidth', 2); + $this->checkStyleVariable($menu, 'borderBottomWidth', 2); + $this->checkStyleVariable($menu, 'borderLeftWidth', 2); + $this->checkStyleVariable($menu, 'borderColour', 'green'); + } + public function test256ColoursCodes() : void { $terminal = static::createMock(Terminal::class); diff --git a/test/MenuStyleTest.php b/test/MenuStyleTest.php index ca861b12..59986a86 100644 --- a/test/MenuStyleTest.php +++ b/test/MenuStyleTest.php @@ -123,6 +123,11 @@ public function testGetterAndSetters() : void static::assertSame(100, $style->getWidth()); static::assertSame(2, $style->getMargin()); static::assertSame(2, $style->getPadding()); + static::assertSame(0, $style->getBorderTopWidth()); + static::assertSame(0, $style->getBorderRightWidth()); + static::assertSame(0, $style->getBorderBottomWidth()); + static::assertSame(0, $style->getBorderLeftWidth()); + static::assertSame('white', $style->getBorderColour()); $style->setBg('red'); $style->setFg('yellow'); @@ -134,6 +139,11 @@ public function testGetterAndSetters() : void $style->setWidth(200); $style->setMargin(10); $style->setPadding(10); + $style->setBorderTopWidth(1); + $style->setBorderRightWidth(2); + $style->setBorderBottomWidth(3); + $style->setBorderLeftWidth(4); + $style->setBorderColour('green'); static::assertSame('red', $style->getBg()); static::assertSame('yellow', $style->getFg()); @@ -145,6 +155,78 @@ public function testGetterAndSetters() : void static::assertSame(200, $style->getWidth()); static::assertSame(10, $style->getMargin()); static::assertSame(10, $style->getPadding()); + static::assertSame(1, $style->getBorderTopWidth()); + static::assertSame(2, $style->getBorderRightWidth()); + static::assertSame(3, $style->getBorderBottomWidth()); + static::assertSame(4, $style->getBorderLeftWidth()); + static::assertSame('green', $style->getBorderColour()); + } + + public function testSetBorderShorthandFunction() : void + { + $style = $this->getMenuStyle(); + $style->setBorder(3); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(3, $style->getBorderRightWidth()); + static::assertSame(3, $style->getBorderBottomWidth()); + static::assertSame(3, $style->getBorderLeftWidth()); + static::assertSame('white', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 4); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(4, $style->getBorderRightWidth()); + static::assertSame(3, $style->getBorderBottomWidth()); + static::assertSame(4, $style->getBorderLeftWidth()); + static::assertSame('white', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 4, 5); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(4, $style->getBorderRightWidth()); + static::assertSame(5, $style->getBorderBottomWidth()); + static::assertSame(4, $style->getBorderLeftWidth()); + static::assertSame('white', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 4, 5, 6); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(4, $style->getBorderRightWidth()); + static::assertSame(5, $style->getBorderBottomWidth()); + static::assertSame(6, $style->getBorderLeftWidth()); + static::assertSame('white', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 4, 5, 6, 'red'); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(4, $style->getBorderRightWidth()); + static::assertSame(5, $style->getBorderBottomWidth()); + static::assertSame(6, $style->getBorderLeftWidth()); + static::assertSame('red', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 4, 5, 'red'); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(4, $style->getBorderRightWidth()); + static::assertSame(5, $style->getBorderBottomWidth()); + static::assertSame(4, $style->getBorderLeftWidth()); + static::assertSame('red', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 4, 'red'); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(4, $style->getBorderRightWidth()); + static::assertSame(3, $style->getBorderBottomWidth()); + static::assertSame(4, $style->getBorderLeftWidth()); + static::assertSame('red', $style->getBorderColour()); + + $style = $this->getMenuStyle(); + $style->setBorder(3, 'red'); + static::assertSame(3, $style->getBorderTopWidth()); + static::assertSame(3, $style->getBorderRightWidth()); + static::assertSame(3, $style->getBorderBottomWidth()); + static::assertSame(3, $style->getBorderLeftWidth()); + static::assertSame('red', $style->getBorderColour()); } public function test256ColoursCodes() : void @@ -196,23 +278,42 @@ public function testGetMarkerReturnsTheCorrectMarkers() : void public function testWidthCalculation() : void { $style = $this->getMenuStyle(); + $style->setPadding(0); + $style->setMargin(0); + $style->setBorder(0); + $style->setWidth(300); + static::assertSame(300, $style->getContentWidth()); + $style->setPadding(5); - $style->setMargin(5); + static::assertSame(290, $style->getContentWidth()); + $style->setMargin(5); static::assertSame(290, $style->getContentWidth()); + + $style->setBorder(2); + static::assertSame(286, $style->getContentWidth()); } public function testRightHandPaddingCalculation() : void { $style = $this->getMenuStyle(); + $style->setPadding(0); + $style->setMargin(0); + $style->setBorder(0); $style->setWidth(300); + static::assertSame(250, $style->getRightHandPadding(50)); + $style->setPadding(5); - $style->setMargin(5); + static::assertSame(245, $style->getRightHandPadding(50)); + $style->setMargin(5); static::assertSame(245, $style->getRightHandPadding(50)); + + $style->setBorder(2); + static::assertSame(241, $style->getRightHandPadding(50)); } public function testMargin() : void