Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bfecc51

Browse files
Add paragraph word wrap support (#219)
* Add paragraph word wrap support * Update CHANGELOG.md
1 parent 2001394 commit bfecc51

9 files changed

Lines changed: 508 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ CHANGELOG
33

44
## master
55

6+
Features:
7+
8+
- Add paragraph word wrap support #219 @KennedyTedesco
9+
610
Improvements:
711

812
- Improve the gauge label style when filled #195 @KennedyTedesco

docs/content/docs/reference/widgets/ParagraphWidget.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Configure the widget using the builder methods named as follows:
2121
| Name | Type | Description |
2222
| --- | --- | --- |
2323
| **style** | `PhpTui\Tui\Style\Style` | |
24-
| **wrap** | `PhpTui\Tui\Extension\Core\Widget\Paragraph\Wrap\|null` | |
24+
| **wrap** | `PhpTui\Tui\Extension\Core\Widget\Paragraph\Wrap` | |
2525
| **text** | `PhpTui\Tui\Text\Text` | |
2626
| **scroll** | `array` | |
2727
| **alignment** | `PhpTui\Tui\Widget\HorizontalAlignment` | |

example/demo/src/Page/BlocksPage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function lorem(): ParagraphWidget
7777

7878
return ParagraphWidget::fromText(
7979
Text::parse(sprintf('<fg=darkgray>%s</>', $text))
80-
)->wrap(Wrap::trimmed());
80+
)->wrap(Wrap::Word);
8181
}
8282

8383
/**

src/Extension/Core/Widget/Paragraph/Wrap.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@
44

55
namespace PhpTui\Tui\Extension\Core\Widget\Paragraph;
66

7-
final class Wrap
7+
enum Wrap
88
{
9-
private function __construct(public bool $trim)
10-
{
11-
}
12-
13-
public static function trimmed(): self
14-
{
15-
return new self(trim: true);
16-
}
9+
case None;
10+
case Word;
11+
case WordTrimmed;
12+
case Character;
1713
}

src/Extension/Core/Widget/ParagraphRenderer.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpTui\Tui\Text\Line;
1212
use PhpTui\Tui\Text\LineComposer;
1313
use PhpTui\Tui\Text\LineComposer\LineTruncator;
14+
use PhpTui\Tui\Text\LineComposer\WordWrapper;
1415
use PhpTui\Tui\Text\Span;
1516
use PhpTui\Tui\Text\StyledGrapheme;
1617
use PhpTui\Tui\Widget\HorizontalAlignment;
@@ -45,12 +46,7 @@ public function render(WidgetRenderer $renderer, Widget $widget, Buffer $buffer,
4546
return [ $graphemes, $line->alignment ?? $widget->alignment ];
4647
}, $widget->text->lines);
4748

48-
$lineComposer = $this->createLineComposer(
49-
$styled,
50-
$textArea,
51-
$widget->wrap,
52-
$widget->scroll[1]
53-
);
49+
$lineComposer = $this->createLineComposer($styled, $textArea, $widget);
5450

5551
$y = 0;
5652
foreach ($lineComposer->nextLine() as $line) {
@@ -84,9 +80,15 @@ public function render(WidgetRenderer $renderer, Widget $widget, Buffer $buffer,
8480
/**
8581
* @param list<array{list<StyledGrapheme>,HorizontalAlignment}> $styled
8682
*/
87-
private function createLineComposer(array $styled, Area $textArea, ?Wrap $wrap, int $horizontalOffset): LineComposer
83+
private function createLineComposer(array $styled, Area $textArea, ParagraphWidget $widget): LineComposer
8884
{
89-
return new LineTruncator($styled, $textArea->width, $horizontalOffset);
85+
$horizontalOffset = $widget->scroll[1];
86+
87+
return match ($widget->wrap) {
88+
Wrap::Word => new WordWrapper($styled, $textArea->width, trim: false),
89+
Wrap::WordTrimmed => new WordWrapper($styled, $textArea->width, trim: true),
90+
default => new LineTruncator($styled, $textArea->width, $horizontalOffset),
91+
};
9092
}
9193

9294
/**

src/Extension/Core/Widget/ParagraphWidget.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class ParagraphWidget implements Widget
2020
/** @param array{int,int} $scroll */
2121
private function __construct(
2222
public Style $style,
23-
public ?Wrap $wrap,
23+
public Wrap $wrap,
2424
public Text $text,
2525
public array $scroll,
2626
public HorizontalAlignment $alignment
@@ -36,7 +36,7 @@ public static function fromText(Text $text): self
3636
{
3737
return new self(
3838
style: Style::default(),
39-
wrap: null,
39+
wrap: Wrap::None,
4040
text: $text,
4141
scroll: [0,0],
4242
alignment: HorizontalAlignment::Left,
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTui\Tui\Text\LineComposer;
6+
7+
use Generator;
8+
use PhpTui\Tui\Text\LineComposer;
9+
use PhpTui\Tui\Text\StyledGrapheme;
10+
use PhpTui\Tui\Widget\HorizontalAlignment;
11+
12+
final class WordWrapper implements LineComposer
13+
{
14+
private const NBSP = "\u{00a0}";
15+
16+
/**
17+
* @param list<array{list<StyledGrapheme>,HorizontalAlignment}> $lines
18+
*/
19+
public function __construct(
20+
private readonly array $lines,
21+
private readonly int $maxLineWidth,
22+
private readonly bool $trim = false,
23+
) {
24+
}
25+
26+
public function nextLine(): Generator
27+
{
28+
if ($this->maxLineWidth === 0) {
29+
return;
30+
}
31+
32+
$wrappedLines = [];
33+
foreach ($this->lines as $line) {
34+
/**
35+
* @var StyledGrapheme[] $lineSymbols
36+
* @var HorizontalAlignment $lineAlignment
37+
*/
38+
[$lineSymbols, $lineAlignment] = $line;
39+
[$currentLine, $currentLineWidth] = [[], 0];
40+
[$currentWord, $currentWordWidth] = [[], 0];
41+
[$whitespaceBuffer, $whitespacesWidth] = [[], 0];
42+
$hasSeenNonWhitespace = false;
43+
44+
foreach ($lineSymbols as $symbol) {
45+
$isWhitespace = $this->isWhitespace($symbol);
46+
$symbolWidth = $symbol->symbolWidth();
47+
if ($symbolWidth > $this->maxLineWidth) {
48+
continue;
49+
}
50+
51+
// Append finished word to current line
52+
if (
53+
$hasSeenNonWhitespace && $isWhitespace
54+
// Append if trimmed (whitespaces removed) word would overflow
55+
|| $this->trim && ($currentWordWidth + $symbolWidth) > $this->maxLineWidth && $currentLine === []
56+
// Append if removed whitespace would overflow -> reset whitespace counting to prevent overflow
57+
|| $this->trim && ($whitespacesWidth + $symbolWidth) > $this->maxLineWidth && $currentLine === []
58+
// Append if complete word would overflow
59+
|| !$this->trim && ($currentWordWidth + $whitespacesWidth + $symbolWidth) > $this->maxLineWidth && $currentLine === []
60+
) {
61+
if ($currentLine !== [] || !$this->trim) {
62+
// Also append whitespaces if not trimming or current line is not empty
63+
$currentLine = [...$currentLine, ...$whitespaceBuffer];
64+
$currentLineWidth += $whitespacesWidth;
65+
}
66+
67+
// Append trimmed word
68+
$currentLine = [...$currentLine, ...$currentWord];
69+
$currentLineWidth += $currentWordWidth;
70+
$currentWord = [];
71+
72+
// Clear whitespace buffer
73+
$whitespaceBuffer = [];
74+
$whitespacesWidth = 0;
75+
$currentWordWidth = 0;
76+
}
77+
78+
if (
79+
// Append the unfinished wrapped line to wrapped lines if it is as wide as max line width
80+
$currentLineWidth >= $this->maxLineWidth
81+
// or if it would be too long with the current partially processed word added
82+
|| ($currentLineWidth + $whitespacesWidth + $currentWordWidth) >= $this->maxLineWidth && $symbolWidth > 0
83+
) {
84+
$remainingWidth = max($this->maxLineWidth - $currentLineWidth, 0);
85+
86+
$wrappedLines[] = $this->processLine($currentLine, $lineAlignment);
87+
$currentLine = [];
88+
$currentLineWidth = 0;
89+
90+
// Remove all whitespaces till end of just appended wrapped line + next whitespace
91+
$this->removeWhitespaces($whitespaceBuffer, $whitespacesWidth, $remainingWidth);
92+
93+
// In case all whitespaces have been exhausted, prevent first whitespace to count towards next word
94+
if ($isWhitespace) {
95+
continue;
96+
}
97+
}
98+
99+
// Append symbol to unfinished, partially processed word
100+
if ($isWhitespace) {
101+
$whitespaceBuffer[] = $symbol;
102+
$whitespacesWidth += $symbolWidth;
103+
} else {
104+
$currentWord[] = $symbol;
105+
$currentWordWidth += $symbolWidth;
106+
}
107+
108+
$hasSeenNonWhitespace = !$isWhitespace;
109+
}
110+
111+
// Append remaining text parts
112+
if ($currentWord !== [] || $whitespaceBuffer !== []) {
113+
if ($currentLine === [] && $currentWord === []) {
114+
$wrappedLines[] = $this->processLine([], $lineAlignment);
115+
} elseif(!$this->trim || $currentLine !== []) {
116+
$currentLine = [...$currentLine, ...$whitespaceBuffer];
117+
$whitespaceBuffer = [];
118+
}
119+
$currentLine = [...$currentLine, ...$currentWord];
120+
}
121+
122+
// Append remaining line
123+
if ($currentLine !== []) {
124+
$wrappedLines[] = $this->processLine($currentLine, $lineAlignment);
125+
}
126+
127+
// Append empty line if there was nothing to wrap in the first place
128+
if ($wrappedLines === []) {
129+
$wrappedLines[] = $this->processLine([], $lineAlignment);
130+
}
131+
}
132+
133+
yield from $wrappedLines;
134+
}
135+
136+
/**
137+
* @param StyledGrapheme[] $currentLine
138+
* @return array{list<StyledGrapheme>,int,HorizontalAlignment}
139+
*/
140+
private function processLine(array $currentLine, HorizontalAlignment $alignment): array
141+
{
142+
$lineWidth = array_reduce($currentLine, function (int $width, StyledGrapheme $grapheme): int {
143+
return $width + $grapheme->symbolWidth();
144+
}, 0);
145+
146+
return [$currentLine, $lineWidth, $alignment];
147+
}
148+
149+
/**
150+
* @param StyledGrapheme[] $whitespaceBuffer
151+
*/
152+
private function removeWhitespaces(array &$whitespaceBuffer, int &$whitespacesWidth, int &$remainingWidth): void
153+
{
154+
$firstWhitespace = array_shift($whitespaceBuffer);
155+
while ($firstWhitespace instanceof StyledGrapheme) {
156+
$symbolWidth = $firstWhitespace->symbolWidth();
157+
$whitespacesWidth -= $symbolWidth;
158+
if ($symbolWidth > $remainingWidth) {
159+
break;
160+
}
161+
$remainingWidth -= $symbolWidth;
162+
$firstWhitespace = array_shift($whitespaceBuffer);
163+
}
164+
}
165+
166+
private function isWhitespace(StyledGrapheme $symbol): bool
167+
{
168+
return preg_match('/\p{Z}/u', $symbol->symbol) && $symbol->symbol !== self::NBSP;
169+
}
170+
}

tests/Example/snapshot/Blocks.snapshot

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
│ [q]uit │ events │ canvas │ chart │ list │ table │ blocks │ sprite │ colors │ │
33
└──────────────────────────────────────────────────────────────────────────────┘
44
┌Borders::ALL──────────────────────────┐Borders::NONE
5-
│Lorem ipsum dolor sit amet, consectetu│Lorem ipsum dolor sit amet, consectetur
6-
r adipiscing elit, sed do eiusmod temp│adipiscing elit, sed do eiusmod tempor i
7-
└──────────────────────────────────────┘ncididunt ut labore et dolore magna aliq
5+
│Lorem ipsum dolor sit amet, │Lorem ipsum dolor sit amet, consectetur
6+
consectetur adipiscing elit, sed do │adipiscing elit, sed do eiusmod tempor
7+
└──────────────────────────────────────┘incididunt ut labore et dolore magna
88
│Borders::LEFT Borders::RIGHT │
99
│Lorem ipsum dolor sit amet, consecteturLorem ipsum dolor sit amet, consectetur│
10-
adipiscing elit, sed do eiusmod tempor adipiscing elit, sed do eiusmod tempor│
11-
incididunt ut labore et dolore magna a incididunt ut labore et dolore magna a
10+
│adipiscing elit, sed do eiusmod tempor adipiscing elit, sed do eiusmod tempor
11+
│incididunt ut labore et dolore magna incididunt ut labore et dolore magna
1212
Borders::TOP────────────────────────────Borders::BOTTOM
1313
Lorem ipsum dolor sit amet, consectetur Lorem ipsum dolor sit amet, consectetur
14-
adipiscing elit, sed do eiusmod tempor iadipiscing elit, sed do eiusmod tempor i
15-
ncididunt ut labore et dolore magna aliq────────────────────────────────────────
14+
adipiscing elit, sed do eiusmod tempor adipiscing elit, sed do eiusmod tempor
15+
incididunt ut labore et dolore magna ────────────────────────────────────────
1616
┌BordersType::Plain────────────────────┐╭BordersType::Rounded──────────────────╮
17-
│Lorem ipsum dolor sit amet, consectetu││Lorem ipsum dolor sit amet, consectetu
18-
r adipiscing elit, sed do eiusmod temp││r adipiscing elit, sed do eiusmod temp
17+
│Lorem ipsum dolor sit amet, ││Lorem ipsum dolor sit amet,
18+
consectetur adipiscing elit, sed do ││consectetur adipiscing elit, sed do
1919
└──────────────────────────────────────┘╰──────────────────────────────────────╯
2020
╚BordersType::Double═══════════════════╗┗BordersType::Thick━━━━━━━━━━━━━━━━━━━━┓

0 commit comments

Comments
 (0)