Entrar
PT
apps/caramail-chat / src / Gui / TabBar.php · commit 6eebcdb
<?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);    }}