theme; $t->colors = [ 'background' => self::$face, 'surface' => '#ffffff', 'text' => '#000000', 'muted' => '#404040', 'border' => self::SHADOW, 'accent' => '#000080', 'accentText' => '#ffffff', 'hover' => '#d4d0c8', 'pressed' => '#a0a0a0', 'focus' => self::SHADOW, 'selection' => '#a6caf0', 'danger' => '#ff0000', 'disabled' => self::SHADOW, 'track' => self::$face, ]; $t->radius = 0; $t->fontSize = 12; $t->controlHeight = 22; $t->padding = 4; } /** The sizes (dp) the interface writes in: 10 the choices and tooltips, 12 everything else, 14 / 16 the talk's big fonts. */ private const SIZES = [10, 11, 12, 13, 14, 16]; /** * The theme's cache is filled with Liberation Sans (Arial's metrics) smoothed with light hinting, at the exact * pixel sizes used (again when the screen's scale changes): the look of the applet as it shows today. */ /** Languages whose letters are not in Liberation Sans: Droid Sans Fallback (assets/fonts/cjk.ttf) instead. */ public const CJK = ['ja', 'zh', 'ko']; public static function crisp(Form $form): void { $sdl = $form->sdlOrNull(); $t = $form->theme; $lang = $form->engineOrNull()?->lang->getLang() ?? 'fr'; $cjk = in_array(substr($lang, 0, 2), self::CJK, true); if ($sdl === null || !function_exists('TTF_OpenFont') && !extension_loaded('sdl_ttf')) { return; } $put = \Closure::bind(function (string $key, \JumPHP\Renderer\TextDrawer $font): void { $this->fonts[$key] = $font; }, $t, Theme::class); foreach (self::SIZES as $dp) { foreach ([false, true] as $bold) { $px = max(6, (int)round($dp * $t->scale)); try { $file = $cjk ? 'assets/fonts/cjk.ttf' : ($bold ? 'assets/fonts/ui-bold.ttf' : 'assets/fonts/ui.ttf'); $put($px . ($bold ? 'b' : ''), self::lightHinted(new \JumPHP\Renderer\TtfFont($sdl, $file, $px, true, $bold && $cjk ? \JumPHP\Renderer\TtfFont::BOLD : 0))); } catch (\Throwable) { return; } } } } /** * The applet as it shows today (a 2002 Java applet in a browser): Arial 12 px smoothed in grey levels, its stems a * little soft. FreeType's light hinting (TTF_HINTING_LIGHT) gives that; its normal hinting is sharper and darker. */ public static function lightHinted(\JumPHP\Renderer\TtfFont $font): \JumPHP\Renderer\TtfFont { if (function_exists('TTF_SetFontHinting')) { \Closure::bind(function (): void { if ($this->font) { \TTF_SetFontHinting($this->font, \defined('TTF_HINTING_LIGHT') ? \TTF_HINTING_LIGHT : 1); } }, $font, \JumPHP\Renderer\TtfFont::class)(); } return $font; } /** Calls crisp() when the scale changed since $seen (each frame from the forms' tick()). */ public static function keepCrisp(Form $form, float &$seen): void { if ($form->theme->scale !== $seen) { $seen = $form->theme->scale; self::crisp($form); $form->invalidate(); // measured again with these fonts (another language: other widths) } } /** A raised bevel (a button, a scrollbar's thumb); $down: pressed in. */ public static function raised(Renderer $r, float $x, float $y, float $w, float $h, ?string $fill = null, bool $down = false, float $px = 1): void { $r->fillRect($x, $y, $w, $h, $fill ?? self::$face); $x2 = $x + $w - $px; $y2 = $y + $h - $px; if ($down) { $r->fillRect($x, $y, $w, $px, self::DARK); $r->fillRect($x, $y, $px, $h, self::DARK); $r->fillRect($x + $px, $y + $px, $w - 2 * $px, $px, self::SHADOW); $r->fillRect($x + $px, $y + $px, $px, $h - 2 * $px, self::SHADOW); $r->fillRect($x, $y2, $w, $px, self::LIGHT); $r->fillRect($x2, $y, $px, $h, self::LIGHT); return; } $r->fillRect($x, $y, $w - $px, $px, self::LIGHT); $r->fillRect($x, $y, $px, $h - $px, self::LIGHT); $r->fillRect($x, $y2, $w, $px, self::DARK); $r->fillRect($x2, $y, $px, $h, self::DARK); $r->fillRect($x + $px, $y2 - $px, $w - 2 * $px, $px, self::SHADOW); $r->fillRect($x2 - $px, $y + $px, $px, $h - 2 * $px, self::SHADOW); } /** A sunken bevel around a field (a text field, a list, a check box): drawn on its edges. */ public static function sunken(Renderer $r, float $x, float $y, float $w, float $h, float $px = 1): void { $r->fillRect($x, $y, $w - $px, $px, self::SHADOW); $r->fillRect($x, $y, $px, $h - $px, self::SHADOW); $r->fillRect($x + $px, $y + $px, $w - 3 * $px, $px, self::DARK); $r->fillRect($x + $px, $y + $px, $px, $h - 3 * $px, self::DARK); $r->fillRect($x, $y + $h - $px, $w, $px, self::LIGHT); $r->fillRect($x + $w - $px, $y, $px, $h, self::LIGHT); $r->fillRect($x + $px, $y + $h - 2 * $px, $w - 2 * $px, $px, self::$face); $r->fillRect($x + $w - 2 * $px, $y + $px, $px, $h - 2 * $px, self::$face); } /** The etched line of the applet's titled panels (net.spray.chat.a.a): grey then white, one pixel apart. */ public static function etched(Renderer $r, float $x, float $y, float $w, float $h, float $px = 1): void { $r->drawRect($x + $px, $y + $px, $w - $px, $h - $px, self::LIGHT, (int)$px); $r->drawRect($x, $y, $w - $px, $h - $px, self::SHADOW, (int)$px); } /** A small black triangle (the arrows of the scrollbars and drop-down lists). $dir: up, down, left, right. */ public static function arrow(Renderer $r, float $cx, float $cy, string $dir, float $px = 1, string $color = self::DARK): void { for ($i = 0; $i < 4; $i++) { $len = (7 - 2 * $i) * $px; match ($dir) { 'up' => $r->fillRect($cx - $len / 2, $cy + (1 - $i) * $px, $len, $px, $color), 'down' => $r->fillRect($cx - $len / 2, $cy + ($i - 2) * $px, $len, $px, $color), 'left' => $r->fillRect($cx + (1 - $i) * $px, $cy - $len / 2, $px, $len, $color), default => $r->fillRect($cx + ($i - 2) * $px, $cy - $len / 2, $px, $len, $color), }; } } /** A text greyed out the AWT way: white, then grey one pixel up-left. */ public static function disabledText(Renderer $r, Theme $t, \JumPHP\Renderer\TextDrawer $font, string $text, float $x, float $y): void { $font->draw($r, $text, $x + $t->px(1), $y + $t->px(1), 1, self::LIGHT); $font->draw($r, $text, $x, $y, 1, self::SHADOW); } public static function px(Theme $t): int { return max(1, (int)round($t->scale)); } }