<?phpnamespace App\Gui;use JumPHP\Gui\Pointer;use JumPHP\Gui\Theme;use JumPHP\Renderer\Renderer;/** * The General tab of XenCenter: sections ("General", "Memory", "CPUs", "Boot Options"...) of "key: value" lines, opened and closed * by a click on their header, scrolled. setSections([['title' => 'General', 'rows' => [['Name:', 'web-01'], ...]]]); a value may * hold "\n" (several lines), or be ['bar' => 0.6, 'text' => '…'] (a usage bar). */final class PropertySheet extends Canvas{ public const DEFAULTS = ['height' => 'fill', 'width' => 'fill', 'background' => '#ffffff', 'border' => false, 'keyWidth' => 210]; /** @var list<array{title: string, rows: list<array{0: string, 1: mixed}>}> */ private array $sections = []; /** @var array<string, bool> closed sections */ public array $closed = []; public function setSections(array $sections): void { $this->sections = $sections; } /** @return list<array{title: string, rows: list<array{0: string, 1: mixed}>}> */ public function sections(): array { return $this->sections; } /** The value of a line (tests): value('General', 'Name:'). */ public function value(string $section, string $key): mixed { foreach ($this->sections as $s) { if ($s['title'] === $section) { foreach ($s['rows'] as [$k, $v]) { if ($k === $key) { return $v; } } } } return null; } private function lineHeight(Theme $t): float { return $t->font()->lineHeight() + $t->px(9); } private function headerHeight(Theme $t): float { return $t->px(32); } /** @return list<string> */ private static function lines(mixed $value): array { return is_array($value) ? [''] : explode("\n", (string)$value); } protected function contentHeight(Theme $t): float { $h = $t->px(8); foreach ($this->sections as $s) { $h += $this->headerHeight($t) + $t->px(4); if (!($this->closed[$s['title']] ?? false)) { foreach ($s['rows'] as [, $v]) { $h += count(self::lines($v)) * $this->lineHeight($t); } $h += $t->px(10); } } return $h; } protected function paint(Renderer $r, Theme $t): void { $font = $t->font(); $bold = $t->font($t->fontSize * 1.05, true); $y = $this->y + $t->px(8) - $this->scrollY; $lh = $this->lineHeight($t); $keyW = $t->px((float)$this->props['keyWidth']); $left = $this->x + $t->px(2); $right = $this->x + $this->w - $t->px(14); foreach ($this->sections as $s) { $hh = $this->headerHeight($t); $closed = $this->closed[$s['title']] ?? false; if ($y + $hh > $this->y && $y < $this->y + $this->h) { $t->box($r, $left, $y, $right - $left, $hh, '#eef3f9', $t->px(6)); $bold->draw($r, \JumPHP\Util\Lang::t($s['title']), $left + $t->px(28), $y + ($hh - $bold->height()) / 2, 1, '#1d3c6e'); // the open / closed chevron $cx = $left + $t->px(11); $cy = $y + $hh / 2; for ($k = 0; $k < 4; $k++) { $closed ? $r->fillRect($cx + $k * $t->scale, $cy - $t->px(4) + $k * $t->scale, max(1, $t->scale), $t->px(8) - 2 * $k * $t->scale, '#1d3c6e') : $r->fillRect($cx - $t->px(1) + $k * $t->scale, $cy - $t->px(2) + $k * $t->scale, $t->px(9) - 2 * $k * $t->scale, max(1, $t->scale), '#1d3c6e'); } } $y += $hh + $t->px(4); if ($closed) { continue; } foreach ($s['rows'] as [$key, $value]) { $lines = self::lines($value); if ($y + count($lines) * $lh > $this->y && $y < $this->y + $this->h) { $ty = $y + $t->px(4); $r->fillRect($left + $t->px(14), $y + count($lines) * $lh - 1, $right - $left - $t->px(28), 1, '#f1f3f6'); $font->draw($r, self::fit($font, \JumPHP\Util\Lang::t($key), $keyW - $t->px(16)), $left + $t->px(28), $ty, 1, '#6b7280'); if (is_array($value) && isset($value['bar'])) { $bw = min($t->px(260), $right - $left - $keyW - $t->px(120)); $ratio = max(0.0, min(1.0, (float)$value['bar'])); $by = $y + $t->px(6); $bh = $lh - $t->px(12); $t->box($r, $left + $keyW, $by, $bw, $bh, '#e9edf2', $t->px(4)); if ($ratio > 0) { $t->box($r, $left + $keyW, $by, max($t->px(8), $bw * $ratio), $bh, $ratio > 0.9 ? '#e5735f' : '#7cc47f', $t->px(4)); } $font->draw($r, self::fit($font, (string)($value['text'] ?? ''), $right - $left - $keyW - $bw - $t->px(12)), $left + $keyW + $bw + $t->px(12), $ty, 1, '#1f2329'); } else { foreach ($lines as $n => $line) { $font->draw($r, self::fit($font, $line, $right - $left - $keyW - $t->px(14)), $left + $keyW, $ty + $n * $lh, 1, '#1f2329'); } } } $y += count($lines) * $lh; } $y += $t->px(10); } } public function pointerUp(Pointer $p, bool $inside): void { if ($this->popupUp($p) || $this->barUp() || !$inside || $this->form === null) { return; } $t = $this->form->theme; $y = $this->y + $t->px(8) - $this->scrollY; foreach ($this->sections as $s) { $hh = $this->headerHeight($t); if ($p->y >= $y && $p->y < $y + $hh) { $this->closed[$s['title']] = !($this->closed[$s['title']] ?? false); $this->clampScroll($t); return; } $y += $hh + $t->px(4); if (!($this->closed[$s['title']] ?? false)) { foreach ($s['rows'] as [, $v]) { $y += count(self::lines($v)) * $this->lineHeight($t); } $y += $t->px(10); } } $this->emit('click'); }}