<?phpnamespace App\Gui;use JumPHP\Gui\Pointer;use JumPHP\Gui\Theme;use JumPHP\Gui\Widget;use JumPHP\Renderer\Renderer;/** * The applet's tabs (net.spray.chat.a.d), drawn as it drew them: a strip (black for the main tabs, grey for the * sub-tabs), rounded grey tabs with a white edge and a dark right side, the chosen one higher, joined to the page, * its name in bold blue; a hidden tab with news turns orange (java.awt.Color.orange) until it is chosen. Names too * long for the strip are cut with "..". * * Props: strip (the colour of the strip). Event: change (string $key). */final class TabBar extends Widget{ public const DEFAULTS = ['height' => 26, 'width' => 'fill', 'strip' => '#000000']; /** @var list<array{key: string, label: string, closable: bool, news: bool}> */ private array $tabs = []; public string $current = ''; /** @var list<array{0: float, 1: float, 2: string}> x from, x to, key (laid out by render) */ private array $boxes = []; public static function events(): array { return ['change', 'close', 'click']; } public function add(string $key, string $label, bool $closable = false): void { if ($this->has($key)) { return; } $this->tabs[] = ['key' => $key, 'label' => $label, 'closable' => $closable, 'news' => false]; if ($this->current === '') { $this->current = $key; } } public function remove(string $key): void { $this->tabs = array_values(array_filter($this->tabs, static fn ($t) => $t['key'] !== $key)); if ($this->current === $key) { $this->current = $this->tabs[0]['key'] ?? ''; $this->emit('change', $this->current); } } public function has(string $key): bool { foreach ($this->tabs as $tab) { if ($tab['key'] === $key) { return true; } } return false; } public function rename(string $key, string $label): void { foreach ($this->tabs as $i => $tab) { if ($tab['key'] === $key) { $this->tabs[$i]['label'] = $label; } } } public function label(string $key): string { foreach ($this->tabs as $tab) { if ($tab['key'] === $key) { return $tab['label']; } } return ''; } /** Chooses a tab (as a click would) and tells the form. */ public function choose(string $key): void { if (!$this->has($key)) { return; } $this->current = $key; foreach ($this->tabs as $i => $tab) { if ($tab['key'] === $key) { $this->tabs[$i]['news'] = false; } } $this->emit('change', $key); } /** Something new on a hidden tab: it turns orange. */ public function notify(string $key): void { foreach ($this->tabs as $i => $tab) { if ($tab['key'] === $key && $key !== $this->current) { $this->tabs[$i]['news'] = true; } } } public function hasNews(string $key): bool { foreach ($this->tabs as $tab) { if ($tab['key'] === $key) { return $tab['news']; } } return false; } /** @return list<string> */ public function tabKeys(): array { return array_map(static fn ($t) => $t['key'], $this->tabs); } protected function content(Theme $t, float $maxWidth, string $breakpoint): array { return [$maxWidth, $t->px(26)]; } /** A rounded rectangle of radius ~3 px (fillRoundRect of AWT), the bottom corners square. */ private static function roundTop(Renderer $r, Theme $t, float $x, float $y, float $w, float $h, string $color): void { $c = $t->px(2); $r->fillRect($x + $c, $y, $w - 2 * $c, $c, $color); $r->fillRect($x + $t->px(1), $y + $t->px(1), $w - 2 * $t->px(1), $c, $color); $r->fillRect($x, $y + $c, $w, $h - $c, $color); } public function render(Renderer $r, Theme $t): void { $px = Awt::px($t); $plain = $t->font(12); $bold = $t->font(12, true); $o = $this->h - $t->px(2); // the strip's height ("o" of the applet) $r->fillRect($this->x, $this->y, $this->w, $o, (string)$this->props['strip']); $grey = (string)$this->props['strip'] !== '#000000'; // the names are cut until every tab fits (the applet's "long") $limit = 32; do { $boxes = []; $x = $this->x + $t->px(3); foreach ($this->tabs as $tab) { $label = mb_strlen($tab['label']) > $limit ? mb_substr($tab['label'], 0, max(1, $limit - 2)) . '..' : $tab['label']; $w = max($plain->width($label), $bold->width($label)) + $t->px(16); $boxes[] = [$x, $x + $w, $tab, $label]; $x += $w + $t->px(2); } $limit--; } while ($x > $this->x + $this->w && $limit > 2); $this->boxes = []; $chosen = null; foreach ($boxes as $box) { [$x0, $x1, $tab, $label] = $box; $this->boxes[] = [$x0, $x1, $tab['key']]; if ($tab['key'] === $this->current) { $chosen = $box; continue; } self::roundTop($r, $t, $x0 - $px, $this->y + $t->px(3), $x1 - $x0 + $px, $o - $t->px(3), Awt::LIGHT); self::roundTop($r, $t, $x0, $this->y + $t->px(4), $x1 - $x0, $o - $t->px(4), $tab['news'] ? Awt::ORANGE : Awt::$face); $r->fillRect($x1 - $px, $this->y + $t->px(5), $px, $o - $t->px(5), $grey ? Awt::SHADOW : Awt::DARK); $plain->draw($r, $label, $x0 + $t->px(7), $this->y + $t->px(4) + ($o - $t->px(4) - $plain->height()) / 2, 1, '#000000'); } // the line under the tabs, then the chosen tab over it, open towards the page $r->fillRect($this->x, $this->y + $o, $this->w, $this->h - $o, Awt::$face); $r->fillRect($this->x, $this->y + $o, $this->w, $px, Awt::LIGHT); if ($chosen !== null) { [$x0, $x1, , $label] = $chosen; self::roundTop($r, $t, $x0 - $t->px(3), $this->y, $x1 - $x0 + $t->px(5), $this->h, Awt::LIGHT); self::roundTop($r, $t, $x0 - $t->px(2), $this->y + $px, $x1 - $x0 + $t->px(3), $this->h, Awt::$face); $r->fillRect($x1 + $t->px(1), $this->y + $t->px(3), $px, $o - $t->px(2), $grey ? Awt::SHADOW : Awt::DARK); $bold->draw($r, $label, $x0 + $t->px(5), $this->y + ($o - $bold->height()) / 2 + $px, 1, '#0000ff'); } } public function pointerUp(Pointer $p, bool $inside): void { if (!$inside) { return; } foreach ($this->boxes as [$x0, $x1, $key]) { if ($p->x > $x0 && $p->x < $x1) { if ($key !== $this->current) { $this->choose($key); } break; } } $this->click($p); }}