essence-os/desktop/theme.cpp

2111 lines
80 KiB
C++

// This file is part of the Essence operating system.
// It is released under the terms of the MIT license -- see LICENSE.md.
// Written by: nakst.
#define THEME_LAYER_BOX (1)
#define THEME_LAYER_TEXT (2)
#define THEME_LAYER_METRICS (3)
#define THEME_LAYER_PATH (4)
#define THEME_PAINT_SOLID (1)
#define THEME_PAINT_LINEAR_GRADIENT (2)
#define THEME_PAINT_CUSTOM (3)
#define THEME_PAINT_OVERWRITE (4)
#define THEME_PAINT_RADIAL_GRADIENT (5)
#define THEME_PAINT_BIT_PATTERN (6)
#define THEME_LAYER_MODE_BACKGROUND (0)
#define THEME_LAYER_MODE_SHADOW (1)
#define THEME_LAYER_MODE_CONTENT (2)
#define THEME_LAYER_MODE_OVERLAY (3)
#define THEME_OVERRIDE_I8 (1)
#define THEME_OVERRIDE_I16 (2)
#define THEME_OVERRIDE_F32 (3)
#define THEME_OVERRIDE_COLOR (4)
#define THEME_PRIMARY_STATE_ANY (0)
#define THEME_PRIMARY_STATE_IDLE (1)
#define THEME_PRIMARY_STATE_HOVERED (2)
#define THEME_PRIMARY_STATE_PRESSED (3)
#define THEME_PRIMARY_STATE_DISABLED (4)
#define THEME_PRIMARY_STATE_INACTIVE (5) // When the window has been deactivated.
#define THEME_PRIMARY_STATE_MASK (0x000F)
#define THEME_STATE_FOCUSED (1 << 15)
#define THEME_STATE_CHECKED (1 << 14)
#define THEME_STATE_INDETERMINATE (1 << 13)
#define THEME_STATE_DEFAULT_BUTTON (1 << 12)
#define THEME_STATE_SELECTED (1 << 11)
#define THEME_STATE_FOCUSED_ITEM (1 << 10)
#define THEME_STATE_LIST_FOCUSED (1 << 9)
#define THEME_STATE_BEFORE_ENTER (1 << 8)
#define THEME_STATE_AFTER_EXIT (1 << 7)
#define THEME_STATE_CHECK(sequenceHeaderState, requestedState) \
(((sequenceHeaderState & THEME_PRIMARY_STATE_MASK) == (requestedState & THEME_PRIMARY_STATE_MASK) \
|| ((sequenceHeaderState & THEME_PRIMARY_STATE_MASK) == THEME_PRIMARY_STATE_ANY)) \
&& !((sequenceHeaderState & ~THEME_PRIMARY_STATE_MASK) & ~(requestedState & ~THEME_PRIMARY_STATE_MASK))) \
#define THEME_LAYER_BOX_IS_BLURRED (1 << 0)
#define THEME_LAYER_BOX_AUTO_CORNERS (1 << 1)
#define THEME_LAYER_BOX_AUTO_BORDERS (1 << 2)
#define THEME_LAYER_BOX_SHADOW_HIDING (1 << 3)
#define THEME_LAYER_PATH_FILL_EVEN_ODD (1 << 0)
#define THEME_LAYER_PATH_CLOSED (1 << 1)
#define THEME_PATH_FILL_SOLID (1 << 4)
#define THEME_PATH_FILL_CONTOUR (2 << 4)
#define THEME_PATH_FILL_DASHED (3 << 4)
#define THEME_HEADER_SIGNATURE (0x96BC555A)
#define THEME_CHILD_TYPE_ONLY (0)
#define THEME_CHILD_TYPE_FIRST (1)
#define THEME_CHILD_TYPE_LAST (2)
#define THEME_CHILD_TYPE_NONE (3)
#define THEME_CHILD_TYPE_HORIZONTAL (1 << 4)
typedef enum ThemeCursor {
THEME_CURSOR_NORMAL,
THEME_CURSOR_TEXT,
THEME_CURSOR_RESIZE_VERTICAL,
THEME_CURSOR_RESIZE_HORIZONTAL,
THEME_CURSOR_RESIZE_DIAGONAL_1,
THEME_CURSOR_RESIZE_DIAGONAL_2,
THEME_CURSOR_SPLIT_VERTICAL,
THEME_CURSOR_SPLIT_HORIZONTAL,
THEME_CURSOR_HAND_HOVER,
THEME_CURSOR_HAND_DRAG,
THEME_CURSOR_HAND_POINT,
THEME_CURSOR_SCROLL_UP_LEFT,
THEME_CURSOR_SCROLL_UP,
THEME_CURSOR_SCROLL_UP_RIGHT,
THEME_CURSOR_SCROLL_LEFT,
THEME_CURSOR_SCROLL_CENTER,
THEME_CURSOR_SCROLL_RIGHT,
THEME_CURSOR_SCROLL_DOWN_LEFT,
THEME_CURSOR_SCROLL_DOWN,
THEME_CURSOR_SCROLL_DOWN_RIGHT,
THEME_CURSOR_SELECT_LINES,
THEME_CURSOR_DROP_TEXT,
THEME_CURSOR_CROSS_HAIR_PICK,
THEME_CURSOR_CROSS_HAIR_RESIZE,
THEME_CURSOR_MOVE_HOVER,
THEME_CURSOR_MOVE_DRAG,
THEME_CURSOR_ROTATE_HOVER,
THEME_CURSOR_ROTATE_DRAG,
THEME_CURSOR_BLANK,
} ThemeCursor;
typedef struct ThemePaintSolid {
uint32_t color;
} ThemePaintSolid;
typedef struct ThemePaintBitPattern {
uint8_t rows[8]; // 8x8 bit pattern.
uint32_t colors[2];
} ThemePaintBitPattern;
typedef struct ThemeGradientStop {
uint32_t color;
int8_t position;
uint8_t windowColorIndex;
uint16_t _unused1;
} ThemeGradientStop;
typedef struct ThemePaintLinearGradient {
float transform[3];
uint8_t stopCount;
int8_t useGammaInterpolation : 1, useDithering : 1;
uint8_t repeatMode;
uint8_t _unused0;
// Followed by gradient stops.
} ThemePaintLinearGradient;
typedef struct ThemePaintRadialGradient {
float transform[6];
uint8_t stopCount;
int8_t useGammaInterpolation : 1, useDithering : 1;
uint8_t repeatMode;
uint8_t _unused0;
// Followed by gradient stops.
} ThemePaintRadialGradient;
#ifndef IN_DESIGNER
typedef uint32_t (*EsFragmentShaderCallback)(int x, int y, struct StyledBox *box);
typedef struct ThemePaintCustom {
EsFragmentShaderCallback callback;
} ThemePaintCustom;
#else
typedef struct ThemePaintCustom {
} ThemePaintCustom;
#endif
typedef struct ThemeLayerBox {
Rectangle8 borders, offset;
Corners8 corners;
int8_t flags, mainPaintType, borderPaintType;
uint8_t _unused0;
uint32_t _unused1; // Align to 8 bytes for ThemePaintCustom.
// Followed by main paint data, then border paint data.
} ThemeLayerBox;
typedef struct ThemeLayerText {
uint32_t color;
uint16_t _unused0;
int8_t blur;
uint8_t _unused1;
} ThemeLayerText;
typedef struct ThemeLayerIcon {
int8_t align;
uint8_t _unused0;
uint16_t alpha;
Rectangle16 image;
} ThemeLayerIcon;
typedef struct ThemeLayerPathFillContour {
float miterLimit;
uint8_t internalWidth, externalWidth;
uint8_t mode;
} ThemeLayerPathFillContour;
typedef struct ThemeLayerPathFillDash {
ThemeLayerPathFillContour contour;
uint8_t length, gap;
} ThemeLayerPathFillDash;
typedef struct ThemeLayerPathFill {
uint8_t paintAndFillType;
uint8_t dashCount;
uint16_t _unused0;
// Followed by paint data, then fill type specific data.
} ThemeLayerPathFill;
typedef struct ThemeLayerPath {
uint8_t flags, fillCount;
uint16_t alpha, pointCount, _unused0;
// Followed by points (2*3 floats per point), then fills.
} ThemeLayerPath;
typedef struct ThemeLayer {
uint32_t overrideListOffset;
uint16_t overrideCount;
uint16_t _unused;
Rectangle8 offset; // (dpx)
Rectangle8 position; // (percent)
int8_t mode, type;
uint16_t dataByteCount;
// Followed by type-specific data.
} ThemeLayer;
typedef struct ThemeMetrics {
Rectangle16 insets, clipInsets;
uint8_t clipEnabled, cursor;
uint16_t fontFamily; // TODO This needs to be validated when loading.
int16_t preferredWidth, preferredHeight;
int16_t minimumWidth, minimumHeight;
int16_t maximumWidth, maximumHeight;
int16_t gapMajor, gapMinor, gapWrap;
uint32_t textColor, selectedBackground, selectedText, iconColor;
int8_t textAlign, fontWeight;
int16_t textSize, iconSize;
uint8_t textFigures;
bool isItalic, layoutVertical;
} ThemeMetrics;
typedef union ThemeVariant {
int8_t i8;
int16_t i16;
uint32_t u32;
float f32;
} ThemeVariant;
typedef struct ThemeOverride {
uint16_t state, duration;
uint16_t offset;
uint8_t type, _unused;
ThemeVariant data;
} ThemeOverride;
typedef struct ThemeStyle {
// A list of uint32_t, giving offsets to ThemeLayer. First is the metrics layer.
// **This must be the first field in the structure; see the end of Export in util/designer2.cpp.**
uint32_t layerListOffset;
uint32_t id;
uint16_t _unused0;
uint8_t layerCount;
uint8_t _unused1;
Rectangle8 paintOutsets, opaqueInsets, approximateBorders;
} ThemeStyle;
typedef struct ThemeConstant {
uint64_t hash;
char cValue[12];
bool scale;
} ThemeConstant;
typedef struct ThemeHeader {
uint32_t signature;
uint32_t styleCount, constantCount;
// Followed by array of ThemeStyles and then an array of ThemeConstants.
} ThemeHeader;
//////////////////////////////////////////
#define THEME_RECT_WIDTH(_r) ((_r).r - (_r).l)
#define THEME_RECT_HEIGHT(_r) ((_r).b - (_r).t)
#define THEME_RECT_4(x, y, z, w) ((EsRectangle) { (x), (y), (z), (w) })
#define THEME_RECT_VALID(_r) (THEME_RECT_WIDTH(_r) > 0 && THEME_RECT_HEIGHT(_r) > 0)
EsRectangle ThemeRectangleIntersection(EsRectangle a, EsRectangle b) {
if (a.l < b.l) a.l = b.l;
if (a.t < b.t) a.t = b.t;
if (a.r > b.r) a.r = b.r;
if (a.b > b.b) a.b = b.b;
return a;
}
typedef struct ThemePaintData {
int8_t type;
union {
const ThemePaintSolid *solid;
const ThemePaintLinearGradient *linearGradient;
const ThemePaintBitPattern *bitPattern;
const ThemePaintCustom *custom;
};
} ThemePaintData;
typedef struct GradientCache {
#define GRADIENT_CACHE_COUNT (256)
uint32_t colors[GRADIENT_CACHE_COUNT];
#if 0
uint32_t ditherColors[GRADIENT_CACHE_COUNT];
uint32_t ditherThresholds[GRADIENT_CACHE_COUNT];
#endif
#define GRADIENT_COORD_BASE (16)
int start, dx, dy;
int ox, oy;
bool opaque, dithered;
void *context;
} GradientCache;
static const float gaussLookup[] = {
1.000000, 0.999862, 0.999450, 0.998764, 0.997805, 0.996572, 0.995068, 0.993293,
0.991249, 0.988937, 0.986360, 0.983520, 0.980418, 0.977058, 0.973442, 0.969573,
0.965454, 0.961089, 0.956480, 0.951633, 0.946549, 0.941235, 0.935693, 0.929928,
0.923946, 0.917749, 0.911344, 0.904735, 0.897927, 0.890926, 0.883736, 0.876364,
0.868815, 0.861094, 0.853207, 0.845160, 0.836960, 0.828611, 0.820121, 0.811494,
0.802738, 0.793858, 0.784861, 0.775752, 0.766539, 0.757227, 0.747823, 0.738333,
0.728763, 0.719119, 0.709409, 0.699637, 0.689810, 0.679935, 0.670017, 0.660062,
0.650077, 0.640067, 0.630038, 0.619995, 0.609946, 0.599894, 0.589846, 0.579807,
0.569782, 0.559777, 0.549797, 0.539846, 0.529930, 0.520053, 0.510220, 0.500435,
0.490704, 0.481029, 0.471416, 0.461867, 0.452388, 0.442982, 0.433653, 0.424403,
0.415236, 0.406156, 0.397166, 0.388267, 0.379464, 0.370759, 0.362153, 0.353651,
0.345253, 0.336962, 0.328780, 0.320708, 0.312749, 0.304903, 0.297173, 0.289559,
0.282062, 0.274685, 0.267426, 0.260289, 0.253272, 0.246376, 0.239602, 0.232951,
0.226422, 0.220016, 0.213732, 0.207571, 0.201532, 0.195614, 0.189819, 0.184144,
0.178591, 0.173157, 0.167842, 0.162646, 0.157567, 0.152605, 0.147759, 0.143027,
0.138409, 0.133903, 0.129508, 0.125223, 0.121047, 0.116978, 0.113014, 0.109155,
0.105399, 0.101744, 0.098188, 0.094731, 0.091371, 0.088106, 0.084933, 0.081853,
0.078863, 0.075961, 0.073146, 0.070415, 0.067769, 0.065203, 0.062718, 0.060310,
0.057980, 0.055723, 0.053541, 0.051429, 0.049387, 0.047413, 0.045506, 0.043663,
0.041883, 0.040165, 0.038507, 0.036907, 0.035364, 0.033876, 0.032442, 0.031060,
0.029729, 0.028447, 0.027212, 0.026025, 0.024882, 0.023782, 0.022726, 0.021710,
0.020734, 0.019796, 0.018895, 0.018031, 0.017201, 0.016405, 0.015642, 0.014910,
0.014208, 0.013536, 0.012892, 0.012275, 0.011684, 0.011119, 0.010578, 0.010061,
0.009567, 0.009094, 0.008642, 0.008211, 0.007798, 0.007405, 0.007029, 0.006671,
0.006329, 0.006003, 0.005692, 0.005396, 0.005114, 0.004845, 0.004590, 0.004346,
0.004114, 0.003894, 0.003684, 0.003485, 0.003295, 0.003115, 0.002944, 0.002782,
0.002628, 0.002482, 0.002343, 0.002211, 0.002086, 0.001968, 0.001856, 0.001750,
0.001649, 0.001554, 0.001464, 0.001378, 0.001298, 0.001221, 0.001149, 0.001081,
0.001017, 0.000956, 0.000899, 0.000844, 0.000793, 0.000745, 0.000699, 0.000656,
0.000616, 0.000578, 0.000542, 0.000508, 0.000476, 0.000446, 0.000418, 0.000391,
0.000366, 0.000343, 0.000321, 0.000300, 0.000281, 0.000263, 0.000245, 0.000229,
0.000214, 0.000200, 0.000187, 0.000174, 0.000163, 0.000152, 0.000141, 0.000000,
};
#ifndef IN_DESIGNER
struct UIStyleKey {
EsStyleID part;
float scale;
uint16_t stateFlags;
uint16_t _unused1;
};
struct {
bool initialised;
EsBuffer system;
const ThemeHeader *header;
EsPaintTarget cursors;
EsHandle cursorData;
float scale;
HashStore<UIStyleKey, struct UIStyle *> loadedStyles;
uint32_t windowColors[6];
Array<EsStyle> internedStyles;
} theming;
#endif
ES_FUNCTION_OPTIMISE_O2
void ThemeFillRectangle(EsPainter *painter, EsRectangle bounds, ThemePaintData paint, GradientCache *gradient) {
uint32_t *bits = (uint32_t *) painter->target->bits;
int width = painter->target->width;
bounds = ThemeRectangleIntersection(bounds, painter->clip);
if (!THEME_RECT_VALID(bounds)) return;
if (paint.type == THEME_PAINT_SOLID) {
#if !defined(IN_DESIGNER) || defined(DESIGNER2)
_DrawBlock(painter->target->stride, bits, bounds, paint.solid->color, painter->target->fullAlpha);
#else
uint32_t color = paint.solid->color;
if ((color & 0xFF000000) == 0) {
} else if ((color & 0xFF000000) == 0xFF000000) {
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l;
uint32_t *b = bits + x + y * width;
do { *b = color; x++, b++; } while (x < bounds.r);
}
} else {
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l;
uint32_t *b = bits + x + y * width;
do { BlendPixel(b, color, painter->target->fullAlpha); x++, b++; } while (x < bounds.r);
}
}
#endif
} else if (paint.type == THEME_PAINT_LINEAR_GRADIENT) {
#if 0
if (gradient->dithered) {
uint32_t random = 0;
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l, p = (y - gradient->oy) * gradient->dy + (bounds.l - gradient->ox) * gradient->dx + gradient->start;
uint32_t *b = bits + bounds.l + y * width;
do {
random = (random * 1103515245) + 12345;
uintptr_t index = ClampInteger(0, GRADIENT_CACHE_COUNT - 1, p >> GRADIENT_COORD_BASE);
if (random > gradient->ditherThresholds[index]) {
*b = gradient->ditherColors[index];
} else {
*b = gradient->colors[index];
}
x++, b++, p += gradient->dx;
} while (x < bounds.r);
}
} else
#endif
if (gradient->opaque) {
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l, p = (y - gradient->oy) * gradient->dy + (bounds.l - gradient->ox) * gradient->dx + gradient->start;
uint32_t *b = bits + bounds.l + y * width;
do {
*b = gradient->colors[ClampInteger(0, GRADIENT_CACHE_COUNT - 1, p >> GRADIENT_COORD_BASE)];
x++, b++, p += gradient->dx;
} while (x < bounds.r);
}
} else {
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l, p = (y - gradient->oy) * gradient->dy + (bounds.l - gradient->ox) * gradient->dx + gradient->start;
uint32_t *b = bits + bounds.l + y * width;
do {
BlendPixel(b, gradient->colors[ClampInteger(0, GRADIENT_CACHE_COUNT - 1, p >> GRADIENT_COORD_BASE)], painter->target->fullAlpha);
x++, b++, p += gradient->dx;
} while (x < bounds.r);
}
}
#ifndef IN_DESIGNER
} else if (paint.type == THEME_PAINT_CUSTOM) {
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l;
uint32_t *b = bits + bounds.l + y * width;
do {
BlendPixel(b, paint.custom->callback(x, y, (StyledBox *) gradient->context), painter->target->fullAlpha);
x++, b++;
} while (x < bounds.r);
}
#endif
} else if (paint.type == THEME_PAINT_OVERWRITE) {
uint32_t color = paint.solid->color;
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l;
uint32_t *b = bits + x + y * width;
do { *b = color; x++, b++; } while (x < bounds.r);
}
} else if (paint.type == THEME_PAINT_BIT_PATTERN) {
for (int y = bounds.t; y < bounds.b; y++) {
uint8_t row = paint.bitPattern->rows[y & 7];
for (int x = bounds.l; x < bounds.r; x++) {
uint32_t *b = bits + x + y * width;
*b = paint.bitPattern->colors[(row >> (x & 7)) & 1];
}
}
}
}
ES_FUNCTION_OPTIMISE_O2
void ThemeFillCorner(EsPainter *painter, EsRectangle bounds, int cx, int cy,
int border, int corner, ThemePaintData mainPaint, ThemePaintData borderPaint,
GradientCache *mainGradient, GradientCache *borderGradient) {
uint32_t *bits = (uint32_t *) painter->target->bits;
int width = painter->target->width;
int oldLeft = bounds.l, oldTop = bounds.t;
bounds = ThemeRectangleIntersection(bounds, painter->clip);
if (!THEME_RECT_VALID(bounds)) return;
cx += oldLeft - bounds.l, cy += oldTop - bounds.t;
#define STYLE_CORNER_OVERSAMPLING (3)
border <<= STYLE_CORNER_OVERSAMPLING;
corner <<= STYLE_CORNER_OVERSAMPLING;
cx <<= STYLE_CORNER_OVERSAMPLING;
cy <<= STYLE_CORNER_OVERSAMPLING;
int mainRadius2 = corner > border ? corner - border : 0;
int borderRadius2 = corner * corner;
mainRadius2 *= mainRadius2;
int oversampledWidth = THEME_RECT_WIDTH(bounds) << STYLE_CORNER_OVERSAMPLING;
int oversampledHeight = THEME_RECT_HEIGHT(bounds) << STYLE_CORNER_OVERSAMPLING;
uint32_t *b0 = bits + bounds.t * width;
for (int j = 0; j < oversampledHeight; j += (1 << STYLE_CORNER_OVERSAMPLING), b0 += width) {
int i = 0;
uint32_t *b = b0 + bounds.l;
do {
int mainCount = 0, borderCount = 0, outsideCount = 0;
for (int x = 0; x < (1 << STYLE_CORNER_OVERSAMPLING); x++) {
for (int y = 0; y < (1 << STYLE_CORNER_OVERSAMPLING); y++) {
int dx = i + x - cx, dy = j + y - cy;
int radius2 = dx * dx + dy * dy;
if (radius2 <= mainRadius2) mainCount++;
else if (radius2 <= borderRadius2) borderCount++;
else outsideCount++;
}
}
uint32_t mainColor, borderColor;
if (mainPaint.type == THEME_PAINT_SOLID || mainPaint.type == THEME_PAINT_OVERWRITE) {
mainColor = mainPaint.solid->color;
} else if (mainPaint.type == THEME_PAINT_BIT_PATTERN) {
uintptr_t tx = ((i >> STYLE_CORNER_OVERSAMPLING) + bounds.l) & 7;
uintptr_t ty = ((j >> STYLE_CORNER_OVERSAMPLING) + bounds.t) & 7;
mainColor = mainPaint.bitPattern->colors[(mainPaint.bitPattern->rows[ty] >> tx) & 1];
} else if (mainPaint.type == THEME_PAINT_LINEAR_GRADIENT) {
mainColor = mainGradient->colors[ClampInteger(0, GRADIENT_CACHE_COUNT - 1,
(((j >> STYLE_CORNER_OVERSAMPLING) - mainGradient->oy + bounds.t) * mainGradient->dy
+ ((i >> STYLE_CORNER_OVERSAMPLING) - mainGradient->ox + bounds.l) * mainGradient->dx + mainGradient->start)
>> GRADIENT_COORD_BASE)];
#ifndef IN_DESIGNER
} else if (mainPaint.type == THEME_PAINT_CUSTOM) {
mainColor = mainPaint.custom->callback((i >> STYLE_CORNER_OVERSAMPLING) - mainGradient->ox + bounds.l,
(j >> STYLE_CORNER_OVERSAMPLING) - mainGradient->oy + bounds.t, (StyledBox *) mainGradient->context);
#endif
} else {
mainColor = 0;
}
if (borderPaint.type == THEME_PAINT_SOLID || borderPaint.type == THEME_PAINT_OVERWRITE) {
borderColor = borderPaint.solid->color;
} else if (borderPaint.type == THEME_PAINT_LINEAR_GRADIENT) {
borderColor = borderGradient->colors[ClampInteger(0, GRADIENT_CACHE_COUNT - 1,
(((j >> STYLE_CORNER_OVERSAMPLING) - borderGradient->oy + bounds.t) * borderGradient->dy
+ ((i >> STYLE_CORNER_OVERSAMPLING) - borderGradient->ox + bounds.l) * borderGradient->dx + borderGradient->start)
>> GRADIENT_COORD_BASE)];
} else {
borderColor = 0;
}
uint32_t mainAlpha = mainColor >> 24;
uint32_t borderAlpha = borderColor >> 24;
if (outsideCount == (1 << (2 * STYLE_CORNER_OVERSAMPLING))) {
} else if (mainPaint.type == THEME_PAINT_OVERWRITE) {
// TODO Support borders when using an overwrite main paint.
// TODO Anti-aliasing (if it's possible).
if (mainCount > (1 << (2 * STYLE_CORNER_OVERSAMPLING - 1))) {
*b = mainColor;
}
} else if (outsideCount || ((borderColor & 0xFF000000) != 0xFF000000) || (mainColor & 0xFF000000) != 0xFF000000) {
BlendPixel(b, (mainColor & 0x00FFFFFF) | (((mainAlpha * mainCount) << (24 - STYLE_CORNER_OVERSAMPLING * 2)) & 0xFF000000),
painter->target->fullAlpha);
BlendPixel(b, (borderColor & 0x00FFFFFF) | (((borderAlpha * borderCount) << (24 - STYLE_CORNER_OVERSAMPLING * 2)) & 0xFF000000),
painter->target->fullAlpha);
} else if (mainCount == (1 << (2 * STYLE_CORNER_OVERSAMPLING))) {
BlendPixel(b, mainColor, painter->target->fullAlpha);
} else if (borderCount == (1 << (2 * STYLE_CORNER_OVERSAMPLING))) {
BlendPixel(b, borderColor, painter->target->fullAlpha);
} else {
uint32_t blend = mainColor;
BlendPixel(&blend, (borderColor & 0x00FFFFFF) | (((borderAlpha * borderCount) << (24 - STYLE_CORNER_OVERSAMPLING * 2)) & 0xFF000000), true);
BlendPixel(b, blend, painter->target->fullAlpha);
}
i += (1 << STYLE_CORNER_OVERSAMPLING), b++;
} while (i < oversampledWidth);
}
}
ES_FUNCTION_OPTIMISE_O2
void ThemeFillBlurCorner(EsPainter *painter, EsRectangle bounds, int cx, int cy, int border, int corner, GradientCache *gradient) {
uint32_t *bits = (uint32_t *) painter->target->bits;
int width = painter->target->width;
cx += bounds.l, cy += bounds.t;
bounds = ThemeRectangleIntersection(bounds, painter->clip);
if (!THEME_RECT_VALID(bounds)) {
return;
}
int dp = (GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE) / border;
int mainRadius = corner > border ? corner - border : 0;
for (int y = bounds.t; y < bounds.b; y++) {
int x = bounds.l;
uint32_t *b = bits + x + y * width;
do {
int dx = x - cx, dy = y - cy;
int radius2 = dx * dx + dy * dy;
int p = (EsCRTsqrtf(radius2) - mainRadius) * dp;
BlendPixel(b, gradient->colors[ClampInteger(0, GRADIENT_CACHE_COUNT - 1, p >> GRADIENT_COORD_BASE)], painter->target->fullAlpha);
x++, b++;
} while (x < bounds.r);
}
}
uint32_t WindowColorCalculate(uint8_t index) {
#ifdef IN_DESIGNER
uint32_t windowColors[6] = {
0xFF83B8F7, 0xFF6D9CDF, 0xFF4166B5, // Active container window gradient stops.
0xFFD2D0F2, 0xFFCDCCFA, 0xFFB5BEDF, // Inactive container window gradient stops.
};
#else
uint32_t *windowColors = theming.windowColors;
#endif
if (index >= 30 && index < 60) {
return EsColorInterpolate(windowColors[0], windowColors[3], (index - 30) / 29.0f);
} else if (index >= 60 && index < 90) {
return EsColorInterpolate(windowColors[1], windowColors[4], (index - 60) / 29.0f);
} else if (index >= 90 && index < 120) {
return EsColorInterpolate(windowColors[2], windowColors[5], (index - 90) / 29.0f);
} else {
return 0;
}
}
ES_FUNCTION_OPTIMISE_O2
void GradientCacheSetup(GradientCache *cache, const ThemePaintLinearGradient *gradient, int width, int height, EsBuffer *data) {
if (!gradient) {
return;
}
width--, height--;
cache->dx = gradient->transform[0] / width * (GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE);
cache->dy = gradient->transform[1] / height * (GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE);
cache->start = gradient->transform[2] * (GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE);
cache->opaque = true;
cache->dithered = gradient->useDithering;
if (!gradient->stopCount) {
return;
}
const ThemeGradientStop *stop0 = (ThemeGradientStop *) EsBufferRead(data, sizeof(ThemeGradientStop));
for (uintptr_t stop = 0; stop < (uintptr_t) (gradient->stopCount - 1); stop++) {
const ThemeGradientStop *stop1 = (ThemeGradientStop *) EsBufferRead(data, sizeof(ThemeGradientStop));
if (!stop1) {
return;
}
uint32_t color0 = stop0->color;
uint32_t color1 = stop1->color;
if (stop0->windowColorIndex) color0 = WindowColorCalculate(stop0->windowColorIndex);
if (stop1->windowColorIndex) color1 = WindowColorCalculate(stop1->windowColorIndex);
float fa = ((color0 >> 24) & 0xFF) / 255.0f;
float fb = ((color0 >> 16) & 0xFF) / 255.0f;
float fg = ((color0 >> 8) & 0xFF) / 255.0f;
float fr = ((color0 >> 0) & 0xFF) / 255.0f;
float ta = ((color1 >> 24) & 0xFF) / 255.0f;
float tb = ((color1 >> 16) & 0xFF) / 255.0f;
float tg = ((color1 >> 8) & 0xFF) / 255.0f;
float tr = ((color1 >> 0) & 0xFF) / 255.0f;
if (fa && !ta) { tr = fr, tg = fg, tb = fb; }
if (ta && !fa) { fr = tr, fg = tg, fb = tb; }
int fi = GRADIENT_CACHE_COUNT * (stop == 0 ? 0 : stop0->position / 100.0);
int ti = GRADIENT_CACHE_COUNT * (stop == (uintptr_t) (gradient->stopCount - 2) ? 1 : stop1->position / 100.0);
if (fi < 0) fi = 0;
if (ti > GRADIENT_CACHE_COUNT) ti = GRADIENT_CACHE_COUNT;
for (int i = fi; i < ti; i++) {
float p = (float) (i - fi) / (ti - fi - 1);
if (p < 0) p = 0;
if (p > 1) p = 1;
if (gradient->useGammaInterpolation) {
cache->colors[i] = (uint32_t) (GammaInterpolate(fr, tr, p) * 255.0f) << 0
| (uint32_t) (GammaInterpolate(fg, tg, p) * 255.0f) << 8
| (uint32_t) (GammaInterpolate(fb, tb, p) * 255.0f) << 16
| (uint32_t) ((fa + (ta - fa) * p) * 255.0f) << 24;
} else {
cache->colors[i] = (uint32_t) (LinearInterpolate(fr, tr, p) * 255.0f) << 0
| (uint32_t) (LinearInterpolate(fg, tg, p) * 255.0f) << 8
| (uint32_t) (LinearInterpolate(fb, tb, p) * 255.0f) << 16
| (uint32_t) ((fa + (ta - fa) * p) * 255.0f) << 24;
}
if ((cache->colors[i] & 0xFF000000) != 0xFF000000) {
cache->opaque = false;
}
}
stop0 = stop1;
}
#if 0
if (gradient->useDithering) {
for (uintptr_t i = 0; i < GRADIENT_CACHE_COUNT; i++) {
uint32_t mainColor = cache->colors[i];
uint32_t ditherColor = mainColor;
int forwardSteps = 0, totalSteps = 0;
for (uintptr_t j = i + 1; j < GRADIENT_CACHE_COUNT; j++) {
forwardSteps++;
if (cache->colors[j] != mainColor) {
ditherColor = cache->colors[j];
break;
}
}
totalSteps = forwardSteps;
for (intptr_t j = i - 1; j >= 0; j--) {
totalSteps++;
if (cache->colors[j] != mainColor) {
break;
}
}
if (!totalSteps) {
totalSteps = 1;
}
cache->ditherColors[i] = ditherColor;
cache->ditherThresholds[i] = (uint32_t) ((float) forwardSteps / totalSteps * 65535.0f) << 16;
}
}
#endif
}
void ThemeDrawBox(EsPainter *painter, EsRectangle rect, EsBuffer *data, float scale,
const ThemeLayer *layer, EsRectangle opaqueRegion, int childType) {
const ThemeLayerBox *box = (const ThemeLayerBox *) EsBufferRead(data, sizeof(ThemeLayerBox));
if (!box) return;
if ((box->flags & THEME_LAYER_BOX_SHADOW_HIDING) && layer->offset.l == 0 && layer->offset.r == 0 && layer->offset.t == 0 && layer->offset.b == 0
&& box->offset.l == 0 && box->offset.r == 0 && box->offset.t == 0 && box->offset.b == 0
&& THEME_RECT_VALID(opaqueRegion)) {
return;
}
rect.l += box->offset.l * scale;
rect.r += box->offset.r * scale;
rect.t += box->offset.t * scale;
rect.b += box->offset.b * scale;
int width = THEME_RECT_WIDTH(rect), height = THEME_RECT_HEIGHT(rect);
#ifdef IN_DESIGNER
if (!THEME_RECT_VALID(UIRectangleIntersection(rect, painter->clip))) {
#else
if (!THEME_RECT_VALID(EsRectangleIntersection(rect, painter->clip))) {
#endif
return;
}
bool isBlurred = box->flags & THEME_LAYER_BOX_IS_BLURRED;
ThemePaintData mainPaint, borderPaint;
GradientCache mainGradient, borderGradient;
mainPaint.type = box->mainPaintType;
borderPaint.type = box->borderPaintType;
if (mainPaint.type == 0) {
} else if (mainPaint.type == THEME_PAINT_SOLID || mainPaint.type == THEME_PAINT_OVERWRITE) {
mainPaint.solid = (const ThemePaintSolid *) EsBufferRead(data, sizeof(ThemePaintSolid));
} else if (mainPaint.type == THEME_PAINT_BIT_PATTERN) {
mainPaint.bitPattern = (const ThemePaintBitPattern *) EsBufferRead(data, sizeof(ThemePaintBitPattern));
} else if (mainPaint.type == THEME_PAINT_LINEAR_GRADIENT) {
mainPaint.linearGradient = (const ThemePaintLinearGradient *) EsBufferRead(data, sizeof(ThemePaintLinearGradient));
GradientCacheSetup(&mainGradient, mainPaint.linearGradient, width, height, data);
mainGradient.ox = rect.l;
mainGradient.oy = rect.t;
} else if (mainPaint.type == THEME_PAINT_CUSTOM && data->context) {
mainPaint.custom = (const ThemePaintCustom *) EsBufferRead(data, sizeof(ThemePaintCustom));
mainGradient.ox = rect.l;
mainGradient.oy = rect.t;
mainGradient.context = data->context;
} else {
data->error = true;
}
if (borderPaint.type == 0) {
} else if (borderPaint.type == THEME_PAINT_SOLID || borderPaint.type == THEME_PAINT_OVERWRITE) {
borderPaint.solid = (ThemePaintSolid *) EsBufferRead(data, sizeof(ThemePaintSolid));
} else if (borderPaint.type == THEME_PAINT_LINEAR_GRADIENT) {
borderPaint.linearGradient = (ThemePaintLinearGradient *) EsBufferRead(data, sizeof(ThemePaintLinearGradient));
if (!data->error) GradientCacheSetup(&borderGradient, borderPaint.linearGradient, width, height, data);
borderGradient.ox = rect.l;
borderGradient.oy = rect.t;
} else {
data->error = true;
}
if (data->error) {
return;
}
if (mainPaint.type == 0 && borderPaint.type == 0) {
return;
}
if (isBlurred && borderPaint.type == THEME_PAINT_SOLID) {
float alpha = borderPaint.solid->color >> 24;
uint32_t color = borderPaint.solid->color & 0xFFFFFF;
borderGradient.start = 0;
borderGradient.opaque = false;
borderGradient.dithered = false;
for (uintptr_t i = 0; i < GRADIENT_CACHE_COUNT; i++) {
borderGradient.colors[i] = color | (uint32_t) (alpha * gaussLookup[i]) << 24;
}
mainPaint.type = THEME_PAINT_SOLID;
mainPaint.solid = borderPaint.solid;
borderPaint.type = THEME_PAINT_LINEAR_GRADIENT;
borderPaint.linearGradient = NULL;
}
Corners32 corners = { (int) (box->corners.tl * scale + 0.5f), (int) (box->corners.tr * scale + 0.5f),
(int) (box->corners.bl * scale + 0.5f), (int) (box->corners.br * scale + 0.5f) };
if (box->flags & THEME_LAYER_BOX_AUTO_CORNERS) {
bool horizontal = childType & THEME_CHILD_TYPE_HORIZONTAL;
int c = childType & ~THEME_CHILD_TYPE_HORIZONTAL;
if (c == THEME_CHILD_TYPE_FIRST) {
if (horizontal) corners.tr = corners.br = 0; else corners.bl = corners.br = 0;
} else if (c == THEME_CHILD_TYPE_LAST) {
if (horizontal) corners.tl = corners.bl = 0; else corners.tl = corners.tr = 0;
} else if (c != THEME_CHILD_TYPE_ONLY) {
corners.tl = corners.tr = corners.bl = corners.br = 0;
}
}
if (isBlurred) {
corners.bl = corners.tr = corners.br = corners.tl;
}
if (corners.tl + corners.tr > width) { float p = (float) corners.tl / (corners.tl + corners.tr); corners.tl = p * width; corners.tr = (1 - p) * width; }
if (corners.bl + corners.br > width) { float p = (float) corners.bl / (corners.bl + corners.br); corners.bl = p * width; corners.br = (1 - p) * width; }
if (corners.tl + corners.bl > height) { float p = (float) corners.tl / (corners.tl + corners.bl); corners.tl = p * height; corners.bl = (1 - p) * height; }
if (corners.tr + corners.br > height) { float p = (float) corners.tr / (corners.tr + corners.br); corners.tr = p * height; corners.br = (1 - p) * height; }
Rectangle32 borders = { (int) (box->borders.l * scale), (int) (box->borders.r * scale),
(int) (box->borders.t * scale), (int) (box->borders.b * scale) };
if (box->flags & THEME_LAYER_BOX_AUTO_BORDERS) {
bool horizontal = childType & THEME_CHILD_TYPE_HORIZONTAL;
int c = childType & ~THEME_CHILD_TYPE_HORIZONTAL;
if (c == THEME_CHILD_TYPE_FIRST) {
if (horizontal) borders.r = 0; else borders.b = 0;
} else if (c == THEME_CHILD_TYPE_LAST) {
if (horizontal) borders.l = 0; else borders.t = 0;
} else if (c != THEME_CHILD_TYPE_ONLY) {
if (horizontal) borders.l = borders.r = 0; else borders.t = borders.b = 0;
}
}
if (isBlurred) {
borders.r = borders.t = borders.b = borders.l;
}
if (borders.l + borders.r > width) { float p = (float) borders.l / (borders.l + borders.r); borders.l = p * width; borders.r = (1 - p) * width; }
if (borders.t + borders.b > height) { float p = (float) borders.t / (borders.t + borders.b); borders.t = p * height; borders.b = (1 - p) * height; }
if (isBlurred && (!borders.l || !borders.r || !borders.t || !borders.b)) {
return;
}
Corners32 cornerBorders = { MaximumInteger(borders.l, borders.t), MaximumInteger(borders.t, borders.r),
MaximumInteger(borders.l, borders.b), MaximumInteger(borders.b, borders.r) };
if (isBlurred) {
borderGradient.dx = -(GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE) / borders.l;
borderGradient.ox = rect.l + borders.l;
borderGradient.dy = borderGradient.oy = 0;
}
ThemeFillRectangle(painter, THEME_RECT_4(rect.l, rect.l + borders.l, rect.t + MaximumInteger(borders.t, corners.tl),
rect.b - MaximumInteger(borders.b, corners.bl)), borderPaint, &borderGradient);
if (isBlurred) {
borderGradient.dx = (GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE) / borders.r;
borderGradient.ox = rect.r - borders.r - 1;
}
ThemeFillRectangle(painter, THEME_RECT_4(rect.r - borders.r, rect.r, rect.t + MaximumInteger(borders.t, corners.tr),
rect.b - MaximumInteger(borders.b, corners.br)), borderPaint, &borderGradient);
if (isBlurred) {
borderGradient.dy = -(GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE) / borders.t;
borderGradient.oy = rect.t + borders.t;
borderGradient.dx = borderGradient.ox = 0;
}
ThemeFillRectangle(painter, THEME_RECT_4(rect.l + corners.tl, rect.r - corners.tr, rect.t, rect.t + borders.t), borderPaint, &borderGradient);
if (isBlurred) {
borderGradient.dy = (GRADIENT_CACHE_COUNT << GRADIENT_COORD_BASE) / borders.b;
borderGradient.oy = rect.b - borders.b - 1;
}
ThemeFillRectangle(painter, THEME_RECT_4(rect.l + corners.bl, rect.r - corners.br, rect.b - borders.b, rect.b), borderPaint, &borderGradient);
ThemeFillRectangle(painter, THEME_RECT_4(rect.l + corners.tl, rect.r - corners.tr,
rect.t + borders.t, rect.t + MaximumInteger(MinimumInteger(corners.tl, corners.tr), borders.t)), mainPaint, &mainGradient);
ThemeFillRectangle(painter, THEME_RECT_4(rect.l + (corners.tl > corners.tr ? corners.tl : borders.l),
rect.r - (corners.tl > corners.tr ? borders.r : corners.tr),
rect.t + MaximumInteger(MinimumInteger(corners.tl, corners.tr), borders.t),
rect.t + MaximumInteger3(corners.tl, corners.tr, borders.t)), mainPaint, &mainGradient);
EsRectangle mainBlock = THEME_RECT_4(rect.l + borders.l, rect.r - borders.r,
rect.t + MaximumInteger3(corners.tl, corners.tr, borders.t),
rect.b - MaximumInteger3(corners.bl, corners.br, borders.b));
if ((box->flags & THEME_LAYER_BOX_SHADOW_HIDING) && THEME_RECT_VALID(opaqueRegion)) {
EsRectangle neededBorders = THEME_RECT_4(MaximumInteger(opaqueRegion.l - mainBlock.l, 0), MaximumInteger(mainBlock.r - opaqueRegion.r, 0),
MaximumInteger(opaqueRegion.t - mainBlock.t, 0), MaximumInteger(mainBlock.b - opaqueRegion.b, 0));
ThemeFillRectangle(painter, THEME_RECT_4(mainBlock.l, mainBlock.r, mainBlock.t, mainBlock.t + neededBorders.t), mainPaint, &mainGradient);
ThemeFillRectangle(painter, THEME_RECT_4(mainBlock.l, mainBlock.l + neededBorders.l,
mainBlock.t + neededBorders.t, mainBlock.b - neededBorders.b), mainPaint, &mainGradient);
ThemeFillRectangle(painter, THEME_RECT_4(mainBlock.r - neededBorders.r, mainBlock.r,
mainBlock.t + neededBorders.t, mainBlock.b - neededBorders.b), mainPaint, &mainGradient);
ThemeFillRectangle(painter, THEME_RECT_4(mainBlock.l, mainBlock.r, mainBlock.b - neededBorders.b, mainBlock.b), mainPaint, &mainGradient);
} else {
ThemeFillRectangle(painter, mainBlock, mainPaint, &mainGradient);
}
ThemeFillRectangle(painter, THEME_RECT_4(rect.l + (corners.bl > corners.br ? corners.bl : borders.l),
rect.r - (corners.bl > corners.br ? borders.r : corners.br),
rect.b - MaximumInteger3(corners.bl, corners.br, borders.b),
rect.b - MaximumInteger(MinimumInteger(corners.bl, corners.br), borders.b)), mainPaint, &mainGradient);
ThemeFillRectangle(painter, THEME_RECT_4(rect.l + corners.bl, rect.r - corners.br,
rect.b - MaximumInteger(MinimumInteger(corners.bl, corners.br), borders.b),
rect.b - borders.b), mainPaint, &mainGradient);
if (cornerBorders.tl >= corners.tl) {
ThemeFillRectangle(painter, THEME_RECT_4(rect.l, rect.l + corners.tl, rect.t + corners.tl, rect.t + cornerBorders.tl), borderPaint, &borderGradient);
}
if (cornerBorders.tr >= corners.tr) {
ThemeFillRectangle(painter, THEME_RECT_4(rect.r - corners.tr, rect.r, rect.t + corners.tr, rect.t + cornerBorders.tr), borderPaint, &borderGradient);
}
if (cornerBorders.bl >= corners.bl) {
ThemeFillRectangle(painter, THEME_RECT_4(rect.l, rect.l + corners.bl, rect.b - cornerBorders.bl, rect.b - corners.bl), borderPaint, &borderGradient);
}
if (cornerBorders.br >= corners.br) {
ThemeFillRectangle(painter, THEME_RECT_4(rect.r - corners.br, rect.r, rect.b - cornerBorders.br, rect.b - corners.br), borderPaint, &borderGradient);
}
if (isBlurred) {
ThemeFillBlurCorner(painter, THEME_RECT_4(rect.l, rect.l + corners.tl, rect.t, rect.t + corners.tl),
corners.tl, corners.tl, borders.l, corners.tl, &borderGradient);
ThemeFillBlurCorner(painter, THEME_RECT_4(rect.r - corners.tr, rect.r, rect.t, rect.t + corners.tr),
-1, corners.tr, borders.l, corners.tr, &borderGradient);
ThemeFillBlurCorner(painter, THEME_RECT_4(rect.l, rect.l + corners.bl, rect.b - corners.bl, rect.b),
corners.bl, -1, borders.l, corners.bl, &borderGradient);
ThemeFillBlurCorner(painter, THEME_RECT_4(rect.r - corners.br, rect.r, rect.b - corners.br, rect.b),
-1, -1, borders.l, corners.br, &borderGradient);
} else {
ThemeFillCorner(painter, THEME_RECT_4(rect.l, rect.l + corners.tl, rect.t, rect.t + corners.tl),
corners.tl, corners.tl, cornerBorders.tl, corners.tl, mainPaint, borderPaint, &mainGradient, &borderGradient);
ThemeFillCorner(painter, THEME_RECT_4(rect.r - corners.tr, rect.r, rect.t, rect.t + corners.tr),
0, corners.tr, cornerBorders.tr, corners.tr, mainPaint, borderPaint, &mainGradient, &borderGradient);
ThemeFillCorner(painter, THEME_RECT_4(rect.l, rect.l + corners.bl, rect.b - corners.bl, rect.b),
corners.bl, 0, cornerBorders.bl, corners.bl, mainPaint, borderPaint, &mainGradient, &borderGradient);
ThemeFillCorner(painter, THEME_RECT_4(rect.r - corners.br, rect.r, rect.b - corners.br, rect.b),
0, 0, cornerBorders.br, corners.br, mainPaint, borderPaint, &mainGradient, &borderGradient);
}
}
void ThemeDrawPath(EsPainter *painter, EsRectangle rect, EsBuffer *data, float scale) {
int width = THEME_RECT_WIDTH(rect), height = THEME_RECT_HEIGHT(rect);
if (width <= 0 || height <= 0) return;
const ThemeLayerPath *layer = (const ThemeLayerPath *) EsBufferRead(data, sizeof(ThemeLayerPath));
if (!layer) return;
if (!layer->pointCount) return;
if (!layer->alpha) return;
const float *points = (const float *) EsBufferRead(data, layer->pointCount * 6 * sizeof(float));
if (!points) return;
EsRectangle bounds;
EsRectangleClip(rect, painter->clip, &bounds);
if (!THEME_RECT_VALID(bounds)) {
return;
}
RastVertex scale2 = { width / 100.0f, height / 100.0f };
RastPath path = {};
for (uintptr_t i = 0, j = 0; i < layer->pointCount * 3; i += 3) {
if ((int) i == layer->pointCount * 3 - 3 || points[i * 2 + 2] == -1e6) {
RastPathAppendBezier(&path, (RastVertex *) points + j, i - j + 1, scale2);
RastPathCloseSegment(&path);
j = i + 3;
}
}
RastPathTranslate(&path, rect.l - painter->clip.l, rect.t - painter->clip.t);
RastSurface surface = {};
float xOffset = rect.l - painter->clip.l;
float yOffset = rect.t - painter->clip.t;
if (!RastSurfaceInitialise(&surface, THEME_RECT_WIDTH(painter->clip), THEME_RECT_HEIGHT(painter->clip), false)) {
goto error;
}
for (uintptr_t i = 0; i < layer->fillCount; i++) {
const ThemeLayerPathFill *fill = (const ThemeLayerPathFill *) EsBufferRead(data, sizeof(ThemeLayerPathFill));
if (!fill) return;
RastPaint paint = {};
RastShape shape = {};
if ((fill->paintAndFillType & 0x0F) == THEME_PAINT_SOLID) {
const ThemePaintSolid *p = (const ThemePaintSolid *) EsBufferRead(data, sizeof(ThemePaintSolid));
if (!p) return;
paint.type = RAST_PAINT_SOLID;
paint.solid.color = p->color & 0xFFFFFF;
paint.solid.alpha = (p->color >> 24) / 255.0f;
} else if ((fill->paintAndFillType & 0x0F) == THEME_PAINT_LINEAR_GRADIENT) {
RastGradientStop stops[16];
size_t stopCount = 0;
const ThemePaintLinearGradient *p = (const ThemePaintLinearGradient *) EsBufferRead(data, sizeof(ThemePaintLinearGradient));
if (!p) return;
for (uintptr_t i = 0; i < p->stopCount && i < 16; i++, stopCount++) {
const ThemeGradientStop *stop = (const ThemeGradientStop *) EsBufferRead(data, sizeof(ThemeGradientStop));
if (!stop) return;
stops[i].color = stop->color;
stops[i].position = stop->position / 100.0f;
}
paint.type = RAST_PAINT_LINEAR_GRADIENT;
paint.gradient.repeatMode = (RastRepeatMode) p->repeatMode;
paint.gradient.transform[0] = p->transform[0] / THEME_RECT_WIDTH(rect);
paint.gradient.transform[1] = p->transform[1] / THEME_RECT_HEIGHT(rect);
paint.gradient.transform[2] = p->transform[2] - (paint.gradient.transform[0] * xOffset + paint.gradient.transform[1] * yOffset);
RastGradientInitialise(&paint, stops, stopCount, p->useGammaInterpolation);
} else if ((fill->paintAndFillType & 0x0F) == THEME_PAINT_RADIAL_GRADIENT) {
RastGradientStop stops[16];
size_t stopCount = 0;
const ThemePaintRadialGradient *p = (const ThemePaintRadialGradient *) EsBufferRead(data, sizeof(ThemePaintRadialGradient));
if (!p) return;
for (uintptr_t i = 0; i < p->stopCount && i < 16; i++, stopCount++) {
const ThemeGradientStop *stop = (const ThemeGradientStop *) EsBufferRead(data, sizeof(ThemeGradientStop));
if (!stop) return;
stops[i].color = stop->color;
stops[i].position = stop->position / 100.0f;
}
paint.type = RAST_PAINT_RADIAL_GRADIENT;
paint.gradient.repeatMode = (RastRepeatMode) p->repeatMode;
paint.gradient.transform[0] = p->transform[0] / THEME_RECT_WIDTH(rect);
paint.gradient.transform[1] = p->transform[1] / THEME_RECT_HEIGHT(rect);
paint.gradient.transform[2] = p->transform[2] - (paint.gradient.transform[0] * xOffset + paint.gradient.transform[1] * yOffset);
paint.gradient.transform[3] = p->transform[3] / THEME_RECT_WIDTH(rect);
paint.gradient.transform[4] = p->transform[4] / THEME_RECT_HEIGHT(rect);
paint.gradient.transform[5] = p->transform[5] - (paint.gradient.transform[3] * xOffset + paint.gradient.transform[4] * yOffset);
RastGradientInitialise(&paint, stops, stopCount, p->useGammaInterpolation);
} else {
// TODO Checkboards, angular gradients and noise.
}
if ((fill->paintAndFillType & 0xF0) == THEME_PATH_FILL_SOLID) {
shape = RastShapeCreateSolid(&path);
} else if ((fill->paintAndFillType & 0xF0) == THEME_PATH_FILL_CONTOUR) {
const ThemeLayerPathFillContour *contour = (const ThemeLayerPathFillContour *) EsBufferRead(data, sizeof(ThemeLayerPathFillContour));
if (!contour) return;
RastContourStyle style = {};
style.internalWidth = contour->internalWidth * scale;
style.externalWidth = contour->externalWidth * scale;
style.joinMode = (RastLineJoinMode) ((contour->mode >> 0) & 3);
style.capMode = (RastLineCapMode) ((contour->mode >> 2) & 3);
style.miterLimit = contour->miterLimit * scale;
if (contour->mode & 0x80) {
style.internalWidth = EsCRTfloorf(style.internalWidth);
style.externalWidth = EsCRTfloorf(style.externalWidth);
}
shape = RastShapeCreateContour(&path, style, ~layer->flags & THEME_LAYER_PATH_CLOSED);
} else if ((fill->paintAndFillType & 0xF0) == THEME_PATH_FILL_DASHED) {
RastDash dashes[16];
RastContourStyle styles[16];
size_t styleCount = 0;
for (uintptr_t i = 0; i < fill->dashCount && i < 16; i++, styleCount++) {
const ThemeLayerPathFillDash *dash = (const ThemeLayerPathFillDash *) EsBufferRead(data, sizeof(ThemeLayerPathFillDash));
if (!dash) return;
dashes[i].gap = dash->gap * scale;
dashes[i].length = dash->length * scale;
dashes[i].style = styles + i;
styles[i].internalWidth = dash->contour.internalWidth * scale;
styles[i].externalWidth = dash->contour.externalWidth * scale;
styles[i].joinMode = (RastLineJoinMode) ((dash->contour.mode >> 0) & 3);
styles[i].capMode = (RastLineCapMode) ((dash->contour.mode >> 2) & 3);
styles[i].miterLimit = dash->contour.miterLimit * scale;
if (dash->contour.mode & 0x80) {
styles[i].internalWidth = EsCRTfloorf(styles[i].internalWidth);
styles[i].externalWidth = EsCRTfloorf(styles[i].externalWidth);
}
}
shape = RastShapeCreateDashed(&path, dashes, styleCount, ~layer->flags & THEME_LAYER_PATH_CLOSED);
}
RastSurfaceFill(surface, shape, paint, layer->flags & THEME_LAYER_PATH_FILL_EVEN_ODD);
RastGradientDestroy(&paint);
}
// TODO Use drawn bounding box.
for (int32_t j = painter->clip.t; j < painter->clip.b; j++) {
uint32_t *source = surface.buffer + (j - painter->clip.t) * surface.stride / 4;
uint32_t *destination = (uint32_t *) painter->target->bits + j * painter->target->stride / 4 + painter->clip.l;
for (int32_t i = painter->clip.l; i < painter->clip.r; i++) {
uint32_t s = *source;
if (layer->alpha != 0xFF) {
uint32_t alpha = s >> 24;
if (alpha) {
alpha *= layer->alpha;
alpha >>= 8;
s = (s & 0xFFFFFF) | (alpha << 24);
BlendPixel(destination, s, painter->target->fullAlpha);
}
} else {
BlendPixel(destination, s, painter->target->fullAlpha);
}
destination++, source++;
}
}
error:;
RastSurfaceDestroy(&surface);
RastPathDestroy(&path);
}
void ThemeDrawLayer(EsPainter *painter, EsRectangle _bounds, EsBuffer *data, float scale, EsRectangle opaqueRegion) {
const ThemeLayer *layer = (const ThemeLayer *) EsBufferRead(data, sizeof(ThemeLayer));
if (!layer) {
return;
}
EsRectangle bounds;
bounds.l = _bounds.l + (int) (scale * layer->offset.l) + THEME_RECT_WIDTH(_bounds) * layer->position.l / 100;
bounds.r = _bounds.l + (int) (scale * layer->offset.r) + THEME_RECT_WIDTH(_bounds) * layer->position.r / 100;
bounds.t = _bounds.t + (int) (scale * layer->offset.t) + THEME_RECT_HEIGHT(_bounds) * layer->position.t / 100;
bounds.b = _bounds.t + (int) (scale * layer->offset.b) + THEME_RECT_HEIGHT(_bounds) * layer->position.b / 100;
if (THEME_RECT_WIDTH(bounds) <= 0 || THEME_RECT_HEIGHT(bounds) <= 0) {
if (layer->dataByteCount) {
EsBufferRead(data, layer->dataByteCount - sizeof(ThemeLayer));
}
} else {
if (layer->type == THEME_LAYER_BOX) {
ThemeDrawBox(painter, bounds, data, scale, layer, opaqueRegion, 0);
} else if (layer->type == THEME_LAYER_PATH) {
ThemeDrawPath(painter, bounds, data, scale);
} else if (layer->type == THEME_LAYER_METRICS) {
EsBufferRead(data, sizeof(ThemeMetrics));
}
}
}
//////////////////////////////////////////
#ifndef IN_DESIGNER
typedef struct ThemeAnimatingProperty {
uint16_t offset; // Offset into the theme data.
uint8_t type : 4, beforeEnter : 1, _unused1 : 3; // Interpolation type.
uint8_t _unused0;
uint16_t duration, elapsed; // Milliseconds.
ThemeVariant from; // Value to interpolate from.
} ThemeAnimatingProperty;
typedef struct ThemeAnimation {
Array<ThemeAnimatingProperty> properties;
} ThemeAnimation;
struct UIStyle {
intptr_t referenceCount;
// General information.
uint8_t textAlign;
EsFont font;
EsRectangle insets, borders;
uint16_t preferredWidth, preferredHeight;
int16_t gapMajor, gapMinor, gapWrap;
uint32_t observedStyleStateMask;
EsRectangle paintOutsets, opaqueInsets;
float scale;
ptrdiff_t appearanceIndex; // An optional, custom appearance provided by the application.
// Data.
uint16_t layerDataByteCount;
const ThemeStyle *style;
ThemeMetrics *metrics; // Points to correct position in layer data.
// Followed by overrides, then layer data.
// The overrides store the base value, and the layer data contains the overriden values.
// Loaded styles management.
void CloseReference();
// Painting.
void PaintText(EsPainter *painter, EsElement *element, EsRectangle rectangle, const char *text, size_t textBytes,
uint32_t iconID, uint32_t flags, const EsTextSelection *selectionProperties = nullptr);
void PaintLayers(EsPainter *painter, EsRectangle rectangle, int childType, int whichLayers);
void PaintTextLayers(EsPainter *painter, EsTextPlan *plan, EsRectangle textBounds, const EsTextSelection *selectionProperties);
// Misc.
bool IsRegionCompletelyOpaque(EsRectangle region, int width, int height);
bool IsStateChangeObserved(uint16_t state1, uint16_t state2);
inline void GetTextStyle(EsTextStyle *style);
};
void ThemeInitialise() {
EsMessageMutexCheck();
if (theming.initialised) return;
theming.initialised = true;
EsBuffer data = {};
data.in = (const uint8_t *) EsBundleFind(&bundleDesktop, EsLiteral("Theme.dat"), &data.bytes);
const ThemeHeader *header = (const ThemeHeader *) EsBufferRead(&data, sizeof(ThemeHeader));
EsAssert(header && header->signature == THEME_HEADER_SIGNATURE && header->styleCount && EsBufferRead(&data, sizeof(ThemeStyle)));
theming.system.in = (const uint8_t *) data.in;
theming.system.bytes = data.bytes;
theming.header = header;
theming.scale = api.global->uiScale;
if (!theming.cursorData) {
size_t cursorsBitmapBytes;
const void *cursorsBitmap = EsBundleFind(&bundleDesktop, EsLiteral("Cursors.png"), &cursorsBitmapBytes);
theming.cursorData = EsMemoryCreateShareableRegion(ES_THEME_CURSORS_WIDTH * ES_THEME_CURSORS_HEIGHT * 4);
void *destination = EsMemoryMapObject(theming.cursorData, 0, ES_THEME_CURSORS_WIDTH * ES_THEME_CURSORS_HEIGHT * 4, ES_MEMORY_MAP_OBJECT_READ_WRITE);
LoadImage(cursorsBitmap, cursorsBitmapBytes, destination, ES_THEME_CURSORS_WIDTH, ES_THEME_CURSORS_HEIGHT, true);
EsObjectUnmap(destination);
}
theming.cursors.width = ES_THEME_CURSORS_WIDTH;
theming.cursors.height = ES_THEME_CURSORS_HEIGHT;
theming.cursors.stride = ES_THEME_CURSORS_WIDTH * 4;
theming.cursors.bits = EsMemoryMapObject(theming.cursorData, 0, ES_MEMORY_MAP_OBJECT_ALL, ES_MEMORY_MAP_OBJECT_READ_ONLY);
theming.cursors.fullAlpha = true;
theming.cursors.readOnly = true;
}
const void *GetConstant(const char *cKey, size_t *byteCount, bool *scale) {
ThemeInitialise();
EsBuffer data = theming.system;
const ThemeHeader *header = (const ThemeHeader *) EsBufferRead(&data, sizeof(ThemeHeader));
EsBufferRead(&data, sizeof(ThemeStyle) * header->styleCount);
uint64_t hash = CalculateCRC64(EsLiteral(cKey), 0);
for (uintptr_t i = 0; i < header->constantCount; i++) {
const ThemeConstant *constant = (const ThemeConstant *) EsBufferRead(&data, sizeof(ThemeConstant));
if (!constant) {
EsPrint("Broken theme constants list.\n");
break;
}
if (constant->hash == hash) {
size_t _byteCount = 0;
for (uintptr_t i = 0; i < sizeof(constant->cValue); i++) {
if (constant->cValue[i] == 0) {
break;
} else {
_byteCount++;
}
}
*byteCount = _byteCount;
*scale = constant->scale;
return constant->cValue;
}
}
EsPrint("Could not find theme constant with key \"%z\".\n", cKey);
return nullptr;
}
int GetConstantNumber(const char *cKey) {
size_t byteCount;
bool scale = false;
const void *value = GetConstant(cKey, &byteCount, &scale);
int integer = value ? EsIntegerParse((const char *) value, byteCount) : 0;
if (scale) integer *= theming.scale;
return integer;
}
const char *GetConstantString(const char *cKey) {
size_t byteCount;
bool scale;
const char *value = (const char *) GetConstant(cKey, &byteCount, &scale);
return !value || !byteCount || value[byteCount - 1] ? nullptr : value;
}
void ThemeStyleCopyInlineMetrics(UIStyle *style) {
style->font.family = style->metrics->fontFamily;
style->font.weight = style->metrics->fontWeight;
style->font.flags = style->metrics->isItalic ? ES_FONT_ITALIC : ES_FLAGS_DEFAULT;
style->preferredWidth = style->metrics->preferredWidth;
style->preferredHeight = style->metrics->preferredHeight;
style->gapMajor = style->metrics->gapMajor;
style->gapMinor = style->metrics->gapMinor;
style->gapWrap = style->metrics->gapWrap;
style->insets.l = style->metrics->insets.l;
style->insets.r = style->metrics->insets.r;
style->insets.t = style->metrics->insets.t;
style->insets.b = style->metrics->insets.b;
style->textAlign = style->metrics->textAlign;
}
bool ThemeAnimationComplete(ThemeAnimation *animation) {
return !animation->properties.Length();
}
bool ThemeAnimationStep(ThemeAnimation *animation, int delta) {
bool repaint = false;
for (uintptr_t i = 0; i < animation->properties.Length(); i++) {
ThemeAnimatingProperty *property = &animation->properties[i];
repaint = true;
property->elapsed += delta;
if (property->duration < property->elapsed) {
animation->properties.Delete(i);
i--;
}
}
return repaint;
}
void ThemeAnimationDestroy(ThemeAnimation *animation) {
animation->properties.Free();
}
ThemeVariant ThemeAnimatingPropertyInterpolate(ThemeAnimatingProperty *property, UIStyle *destination, uint8_t *layerData) {
uint32_t dataOffset = property->offset;
float position = (float) property->elapsed / property->duration;
position = SmoothAnimationTime(position);
if (property->type == THEME_OVERRIDE_I8) {
EsAssert(dataOffset <= destination->layerDataByteCount - sizeof(uint8_t));
int8_t to = *(int8_t *) (layerData + dataOffset);
return (ThemeVariant) { .i8 = (int8_t ) LinearInterpolate(property->from.i8, to, position) };
} else if (property->type == THEME_OVERRIDE_I16) {
EsAssert(dataOffset <= destination->layerDataByteCount - sizeof(uint16_t));
int16_t to = *(int16_t *) (layerData + dataOffset);
return (ThemeVariant) { .i16 = (int16_t) LinearInterpolate(property->from.i16, to, position) };
} else if (property->type == THEME_OVERRIDE_F32) {
EsAssert(dataOffset <= destination->layerDataByteCount - sizeof(float));
float to = *(float *) (layerData + dataOffset);
return (ThemeVariant) { .f32 = (float) LinearInterpolate(property->from.f32, to, position) };
} else if (property->type == THEME_OVERRIDE_COLOR) {
EsAssert(dataOffset <= destination->layerDataByteCount - sizeof(uint32_t));
uint32_t to = *(uint32_t *) (layerData + dataOffset);
return (ThemeVariant) { .u32 = EsColorInterpolate(property->from.u32, to, position) };
} else {
EsAssert(false);
return {};
}
}
UIStyle *ThemeStyleInterpolate(UIStyle *source, ThemeAnimation *animation) {
size_t byteCount = sizeof(UIStyle) + source->layerDataByteCount;
UIStyle *destination = (UIStyle *) EsHeapAllocate(byteCount, false);
EsMemoryCopy(destination, source, byteCount);
uint8_t *layerData = (uint8_t *) (destination + 1);
destination->metrics = (ThemeMetrics *) (layerData + sizeof(ThemeLayer));
for (uintptr_t i = 0; i < animation->properties.Length(); i++) {
ThemeAnimatingProperty *property = &animation->properties[i];
ThemeVariant result = ThemeAnimatingPropertyInterpolate(property, destination, layerData);
if (property->type == THEME_OVERRIDE_I8) {
*(int8_t *) (layerData + property->offset) = result.i8;
} else if (property->type == THEME_OVERRIDE_I16) {
*(int16_t *) (layerData + property->offset) = result.i16;
} else if (property->type == THEME_OVERRIDE_F32) {
*(float *) (layerData + property->offset) = result.f32;
} else if (property->type == THEME_OVERRIDE_COLOR) {
*(uint32_t *) (layerData + property->offset) = result.u32;
}
}
ThemeStyleCopyInlineMetrics(destination);
return destination;
}
void _ThemeAnimationBuildAddProperties(ThemeAnimation *animation, UIStyle *style, uint16_t stateFlags) {
const uint32_t *layerList = (const uint32_t *) (theming.system.in + style->style->layerListOffset);
// (Layer list was validated during ThemeStyleInitialise.)
uintptr_t layerCumulativeDataOffset = 0;
uint8_t *oldLayerData = (uint8_t *) (style + 1);
for (uintptr_t i = 0; i < style->style->layerCount; i++) {
const ThemeLayer *layer = (const ThemeLayer *) (theming.system.in + layerList[i]);
EsBuffer layerData = theming.system;
layerData.position = layer->overrideListOffset;
for (uintptr_t i = 0; i < layer->overrideCount; i++) {
const ThemeOverride *themeOverride = (const ThemeOverride *) EsBufferRead(&layerData, sizeof(ThemeOverride));
if (!THEME_STATE_CHECK(themeOverride->state, stateFlags) || !themeOverride->duration) {
continue;
}
uintptr_t key = themeOverride->offset + layerCumulativeDataOffset;
if (themeOverride->type == THEME_OVERRIDE_I8) EsAssert(key <= (uintptr_t) style->layerDataByteCount - sizeof(uint8_t));
if (themeOverride->type == THEME_OVERRIDE_I16) EsAssert(key <= (uintptr_t) style->layerDataByteCount - sizeof(uint16_t));
if (themeOverride->type == THEME_OVERRIDE_F32) EsAssert(key <= (uintptr_t) style->layerDataByteCount - sizeof(float));
if (themeOverride->type == THEME_OVERRIDE_COLOR) EsAssert(key <= (uintptr_t) style->layerDataByteCount - sizeof(uint32_t));
uintptr_t point;
bool alreadyInList;
// Find where the property is/should be in the animation list.
ES_MACRO_SEARCH(animation->properties.Length(), {
uintptr_t item = animation->properties[index].offset;
result = key < item ? -1 : key > item ? 1 : 0;
}, point, alreadyInList);
bool beforeEnter = (themeOverride->state & THEME_STATE_BEFORE_ENTER) != 0;
if (alreadyInList) {
// Update the duration, if the property is already in the list.
// Prioritise before enter sequence durations.
if (!animation->properties[point].beforeEnter || beforeEnter) {
animation->properties[point].duration = themeOverride->duration * api.global->animationTimeMultiplier;
animation->properties[point].beforeEnter = beforeEnter;
}
} else {
// Add the property to the list.
if (point < animation->properties.Length()) EsAssert(key < animation->properties[point].offset);
if (point > 0) EsAssert(key > animation->properties[point - 1].offset);
ThemeAnimatingProperty property = {};
property.offset = key;
property.type = themeOverride->type;
property.duration = themeOverride->duration * api.global->animationTimeMultiplier;
property.beforeEnter = beforeEnter;
if (themeOverride->type == THEME_OVERRIDE_I8) {
EsAssert(themeOverride->offset <= (uintptr_t) layer->dataByteCount - 1);
property.from.i8 = *(int8_t *) (oldLayerData + key);
} else if (themeOverride->type == THEME_OVERRIDE_I16) {
EsAssert(themeOverride->offset <= (uintptr_t) layer->dataByteCount - 2);
property.from.i16 = *(int16_t *) (oldLayerData + key);
} else if (themeOverride->type == THEME_OVERRIDE_F32) {
EsAssert(themeOverride->offset <= (uintptr_t) layer->dataByteCount - 4);
property.from.f32 = *(float *) (oldLayerData + key);
} else if (themeOverride->type == THEME_OVERRIDE_COLOR) {
EsAssert(themeOverride->offset <= (uintptr_t) layer->dataByteCount - 4);
property.from.u32 = *(uint32_t *) (oldLayerData + key);
}
animation->properties.Insert(property, point);
}
}
layerCumulativeDataOffset += layer->dataByteCount;
}
}
void ThemeAnimationBuild(ThemeAnimation *animation, UIStyle *oldStyle, uint16_t oldStateFlags, uint16_t newStateFlags) {
// Interpolate all the animating properties using the old style as the target.
uint8_t *oldLayerData = (uint8_t *) (oldStyle + 1);
for (uintptr_t i = 0; i < animation->properties.Length(); i++) {
ThemeAnimatingProperty *property = &animation->properties[i];
property->from = ThemeAnimatingPropertyInterpolate(property, oldStyle, oldLayerData);
property->elapsed = 0;
}
// Look for the all the sequences that match the old state flags,
// and add properties to return values to base.
_ThemeAnimationBuildAddProperties(animation, oldStyle, oldStateFlags);
// Look for the all the sequences that match the new state flags,
// and add properties to interpolate them to the target sequencs.
_ThemeAnimationBuildAddProperties(animation, oldStyle, newStateFlags);
}
void ThemeStylePrepare(UIStyle *style, EsStyle *esStyle, UIStyleKey key) {
EsThemeMetrics *customMetrics = esStyle ? &esStyle->metrics : nullptr;
const ThemeStyle *themeStyle = style->style;
// Apply custom metrics and appearance.
if (customMetrics) {
#define ES_RECTANGLE_TO_RECTANGLE_8(x) { (int8_t) (x).l, (int8_t) (x).r, (int8_t) (x).t, (int8_t) (x).b }
if (customMetrics->mask & ES_THEME_METRICS_INSETS) style->metrics->insets = ES_RECTANGLE_TO_RECTANGLE_8(customMetrics->insets);
if (customMetrics->mask & ES_THEME_METRICS_CLIP_INSETS) style->metrics->clipInsets = ES_RECTANGLE_TO_RECTANGLE_8(customMetrics->clipInsets);
if (customMetrics->mask & ES_THEME_METRICS_CLIP_ENABLED) style->metrics->clipEnabled = customMetrics->clipEnabled;
if (customMetrics->mask & ES_THEME_METRICS_CURSOR) style->metrics->cursor = customMetrics->cursor;
if (customMetrics->mask & ES_THEME_METRICS_PREFERRED_WIDTH) style->metrics->preferredWidth = customMetrics->preferredWidth;
if (customMetrics->mask & ES_THEME_METRICS_PREFERRED_HEIGHT) style->metrics->preferredHeight = customMetrics->preferredHeight;
if (customMetrics->mask & ES_THEME_METRICS_MINIMUM_WIDTH) style->metrics->minimumWidth = customMetrics->minimumWidth;
if (customMetrics->mask & ES_THEME_METRICS_MINIMUM_HEIGHT) style->metrics->minimumHeight = customMetrics->minimumHeight;
if (customMetrics->mask & ES_THEME_METRICS_MAXIMUM_WIDTH) style->metrics->maximumWidth = customMetrics->maximumWidth;
if (customMetrics->mask & ES_THEME_METRICS_MAXIMUM_HEIGHT) style->metrics->maximumHeight = customMetrics->maximumHeight;
if (customMetrics->mask & ES_THEME_METRICS_GAP_MAJOR) style->metrics->gapMajor = customMetrics->gapMajor;
if (customMetrics->mask & ES_THEME_METRICS_GAP_MINOR) style->metrics->gapMinor = customMetrics->gapMinor;
if (customMetrics->mask & ES_THEME_METRICS_GAP_WRAP) style->metrics->gapWrap = customMetrics->gapWrap;
if (customMetrics->mask & ES_THEME_METRICS_TEXT_COLOR) style->metrics->textColor = customMetrics->textColor;
if (customMetrics->mask & ES_THEME_METRICS_TEXT_FIGURES) style->metrics->textFigures = customMetrics->textFigures;
if (customMetrics->mask & ES_THEME_METRICS_SELECTED_BACKGROUND) style->metrics->selectedBackground = customMetrics->selectedBackground;
if (customMetrics->mask & ES_THEME_METRICS_SELECTED_TEXT) style->metrics->selectedText = customMetrics->selectedText;
if (customMetrics->mask & ES_THEME_METRICS_ICON_COLOR) style->metrics->iconColor = customMetrics->iconColor;
if (customMetrics->mask & ES_THEME_METRICS_TEXT_ALIGN) style->metrics->textAlign = customMetrics->textAlign;
if (customMetrics->mask & ES_THEME_METRICS_TEXT_SIZE) style->metrics->textSize = customMetrics->textSize;
if (customMetrics->mask & ES_THEME_METRICS_FONT_FAMILY) style->metrics->fontFamily = customMetrics->fontFamily;
if (customMetrics->mask & ES_THEME_METRICS_FONT_WEIGHT) style->metrics->fontWeight = customMetrics->fontWeight;
if (customMetrics->mask & ES_THEME_METRICS_ICON_SIZE) style->metrics->iconSize = customMetrics->iconSize;
if (customMetrics->mask & ES_THEME_METRICS_IS_ITALIC) style->metrics->isItalic = customMetrics->isItalic;
if (customMetrics->mask & ES_THEME_METRICS_LAYOUT_VERTICAL) style->metrics->layoutVertical = customMetrics->layoutVertical;
}
style->appearanceIndex = esStyle && esStyle->appearance.enabled ? (ptrdiff_t) (key.part - 1) : -1;
// Apply scaling to the metrics.
int16_t *scale16[] = {
&style->metrics->insets.l, &style->metrics->insets.r, &style->metrics->insets.t, &style->metrics->insets.b,
&style->metrics->clipInsets.l, &style->metrics->clipInsets.r, &style->metrics->clipInsets.t, &style->metrics->clipInsets.b,
&style->metrics->gapMajor, &style->metrics->gapMinor, &style->metrics->gapWrap,
&style->metrics->preferredWidth, &style->metrics->preferredHeight,
&style->metrics->minimumWidth, &style->metrics->minimumHeight,
&style->metrics->maximumWidth, &style->metrics->maximumHeight,
&style->metrics->iconSize,
};
for (uintptr_t i = 0; i < sizeof(scale16) / sizeof(scale16[0]); i++) {
*(scale16[i]) = *(scale16[i]) * key.scale;
}
style->scale = key.scale;
// Copy inline metrics.
style->borders.l = themeStyle->approximateBorders.l * key.scale;
style->borders.r = themeStyle->approximateBorders.r * key.scale;
style->borders.t = themeStyle->approximateBorders.t * key.scale;
style->borders.b = themeStyle->approximateBorders.b * key.scale;
style->paintOutsets.l = EsCRTceilf(themeStyle->paintOutsets.l * key.scale);
style->paintOutsets.r = EsCRTceilf(themeStyle->paintOutsets.r * key.scale);
style->paintOutsets.t = EsCRTceilf(themeStyle->paintOutsets.t * key.scale);
style->paintOutsets.b = EsCRTceilf(themeStyle->paintOutsets.b * key.scale);
if (style->opaqueInsets.l != 0x7F) {
style->opaqueInsets.l = themeStyle->opaqueInsets.l * key.scale;
style->opaqueInsets.r = themeStyle->opaqueInsets.r * key.scale;
style->opaqueInsets.t = themeStyle->opaqueInsets.t * key.scale;
style->opaqueInsets.b = themeStyle->opaqueInsets.b * key.scale;
}
if (style->appearanceIndex != -1) {
EsThemeAppearance *appearance = &theming.internedStyles[style->appearanceIndex].appearance;
if ((appearance->backgroundColor & 0xFF000000) == 0xFF000000) {
style->opaqueInsets = ES_RECT_1(0);
} else {
style->opaqueInsets = ES_RECT_1(0x7F);
}
}
ThemeStyleCopyInlineMetrics(style);
}
UIStyle *ThemeStyleInitialise(UIStyleKey key) {
ThemeInitialise();
// Find the ThemeStyle entry.
EsStyle *esStyle = (key.part & 0x80000000) || (!key.part) ? nullptr : &theming.internedStyles[key.part - 1];
EsStyleID id = (esStyle ? esStyle->inherit : key.part) & ~0x80000000;
EsBuffer data = theming.system;
const ThemeHeader *header = (const ThemeHeader *) EsBufferRead(&data, sizeof(ThemeHeader));
const ThemeStyle *themeStyle = nullptr;
bool found = false;
for (uintptr_t i = 0; i < header->styleCount; i++) {
themeStyle = (const ThemeStyle *) EsBufferRead(&data, sizeof(ThemeStyle));
if (!themeStyle) {
EsPrint("Broken theme styles list.\n");
break;
}
if (themeStyle->id == id) {
found = true;
break;
}
}
if (!found) {
EsPrint("Could not find theme style with ID %d.\n", id);
data.position = sizeof(ThemeHeader);
themeStyle = (const ThemeStyle *) EsBufferRead(&data, sizeof(ThemeStyle));
}
if (!themeStyle->layerCount) {
EsPrint("Style has no layers (must have a metrics layer).\n");
return nullptr;
}
// Get information about the layers.
size_t layerDataByteCount = 0;
data.position = themeStyle->layerListOffset;
for (uintptr_t i = 0; i < themeStyle->layerCount; i++) {
const uint32_t *offset = (const uint32_t *) EsBufferRead(&data, sizeof(uint32_t));
if (!offset) {
EsPrint("Broken style layer list.\n");
return nullptr;
}
EsBuffer layerData = data;
layerData.position = *offset;
const ThemeLayer *layer = (const ThemeLayer *) EsBufferRead(&layerData, sizeof(ThemeLayer));
if (!layer) {
EsPrint("Broken style layer list.\n");
return nullptr;
}
if (layer->dataByteCount < sizeof(ThemeLayer)) {
EsPrint("Broken layer data byte count (%d; %d).\n", layer->dataByteCount, *offset);
return nullptr;
}
layerDataByteCount += layer->dataByteCount;
if (i == 0) {
if (layer->type != THEME_LAYER_METRICS) {
EsPrint("Style does not have metrics layer.\n");
return nullptr;
}
if (layer->dataByteCount != sizeof(ThemeMetrics) + sizeof(ThemeLayer)) {
EsPrint("Broken metrics layer.\n");
return nullptr;
}
const ThemeMetrics *metrics = (const ThemeMetrics *) EsBufferRead(&layerData, sizeof(ThemeMetrics));
if (!metrics) {
EsPrint("Broken metrics layer.\n");
return nullptr;
}
} else {
const uint8_t *data = (const uint8_t *) EsBufferRead(&layerData, layer->dataByteCount);
if (!data) {
EsPrint("Broken layer.\n");
return nullptr;
}
}
layerData.position = layer->overrideListOffset;
for (uintptr_t i = 0; i < layer->overrideCount; i++) {
const ThemeOverride *themeOverride = (const ThemeOverride *) EsBufferRead(&layerData, sizeof(ThemeOverride));
if (!themeOverride) {
EsPrint("Broken override list.\n");
return nullptr;
}
if (!THEME_STATE_CHECK(themeOverride->state, key.stateFlags)) {
continue;
}
if (themeOverride->offset >= layer->dataByteCount) {
EsPrint("Broken override list.\n");
return nullptr;
}
bool valid;
if (themeOverride->type == THEME_OVERRIDE_I8) {
valid = themeOverride->offset + 1 <= layer->dataByteCount;
} else if (themeOverride->type == THEME_OVERRIDE_I16) {
valid = themeOverride->offset + 2 <= layer->dataByteCount;
} else if (themeOverride->type == THEME_OVERRIDE_F32) {
valid = themeOverride->offset + 4 <= layer->dataByteCount;
} else if (themeOverride->type == THEME_OVERRIDE_COLOR) {
valid = themeOverride->offset + 4 <= layer->dataByteCount;
} else {
EsPrint("Unsupported override type.\n");
return nullptr;
}
if (!valid) {
EsPrint("Broken override list.\n");
return nullptr;
}
}
}
if (layerDataByteCount > 0xFFFF) {
EsPrint("Layer data too large.\n");
return nullptr;
}
// Allocate the style.
UIStyle *style = (UIStyle *) EsHeapAllocate(sizeof(UIStyle) + layerDataByteCount, true);
style->referenceCount = 1;
style->layerDataByteCount = layerDataByteCount;
style->style = themeStyle;
layerDataByteCount = 0;
data.position = themeStyle->layerListOffset;
uint8_t *baseData = (uint8_t *) (style + 1);
style->metrics = (ThemeMetrics *) (baseData + sizeof(ThemeLayer));
// Copy overrides and base layer data into the style, and apply overrides.
for (uintptr_t i = 0; i < themeStyle->layerCount; i++) {
const uint32_t *offset = (const uint32_t *) EsBufferRead(&data, sizeof(uint32_t));
EsBuffer layerData = data;
layerData.position = *offset;
const ThemeLayer *layer = (const ThemeLayer *) EsBufferRead(&layerData, sizeof(ThemeLayer));
layerData.position = *offset;
const uint8_t *data = (const uint8_t *) EsBufferRead(&layerData, layer->dataByteCount);
EsMemoryCopy(baseData + layerDataByteCount, data, layer->dataByteCount);
layerData.position = layer->overrideListOffset;
for (uintptr_t i = 0; i < layer->overrideCount; i++) {
const ThemeOverride *themeOverride = (const ThemeOverride *) EsBufferRead(&layerData, sizeof(ThemeOverride));
style->observedStyleStateMask |= 0x10000 << (themeOverride->state & THEME_PRIMARY_STATE_MASK);
style->observedStyleStateMask |= themeOverride->state & ~THEME_PRIMARY_STATE_MASK;
if (!THEME_STATE_CHECK(themeOverride->state, key.stateFlags)) {
continue;
}
ThemeVariant overrideValue = themeOverride->data;
if (themeOverride->type == THEME_OVERRIDE_I8) {
*(int8_t *) (baseData + layerDataByteCount + themeOverride->offset) = overrideValue.i8;
} else if (themeOverride->type == THEME_OVERRIDE_I16) {
*(int16_t *) (baseData + layerDataByteCount + themeOverride->offset) = overrideValue.i16;
} else if (themeOverride->type == THEME_OVERRIDE_F32) {
*(float *) (baseData + layerDataByteCount + themeOverride->offset) = overrideValue.f32;
} else if (themeOverride->type == THEME_OVERRIDE_COLOR) {
*(uint32_t *) (baseData + layerDataByteCount + themeOverride->offset) = overrideValue.u32;
} else {
EsAssert(false);
}
}
layerDataByteCount += layer->dataByteCount;
}
ThemeStylePrepare(style, esStyle, key);
return style;
}
UIStyleKey MakeStyleKey(EsStyleID style, uint16_t stateFlags) {
return { .part = style, .scale = theming.scale, .stateFlags = stateFlags };
}
EsStyleID EsStyleIntern(const EsStyle *style) {
// TODO Faster lookup.
EsMessageMutexCheck();
for (uintptr_t i = 0; i < theming.internedStyles.Length(); i++) {
if (0 == EsMemoryCompare(&theming.internedStyles[i], style, sizeof(EsStyle))) {
return i + 1;
}
}
theming.internedStyles.AddPointer(style);
return theming.internedStyles.Length();
}
void FreeUnusedStyles(bool includePermanentStyles) {
for (uintptr_t i = 0; i < theming.loadedStyles.Count(); i++) {
UIStyle *style = theming.loadedStyles[i];
if (style->referenceCount == 0 || (style->referenceCount == -1 && includePermanentStyles)) {
UIStyleKey key = theming.loadedStyles.KeyAtIndex(i);
theming.loadedStyles.Delete(&key);
EsHeapFree(style);
i--;
}
}
}
UIStyle *GetStyle(UIStyleKey key, bool keepAround) {
UIStyle **style = theming.loadedStyles.Get(&key);
if (!style) {
style = theming.loadedStyles.Put(&key);
*style = ThemeStyleInitialise(key);
EsAssert(style);
} else if ((*style)->referenceCount != -1) {
(*style)->referenceCount++;
}
if (keepAround || !key.part) {
(*style)->referenceCount = -1;
}
return *style;
}
void GetPreferredSizeFromStylePart(EsStyleID esStyle, int64_t *width, int64_t *height) {
UIStyle *style = GetStyle(MakeStyleKey(esStyle, ES_FLAGS_DEFAULT), true);
if (width) *width = style->preferredWidth;
if (height) *height = style->preferredHeight;
style->CloseReference();
}
void UIStyle::CloseReference() {
if (referenceCount == -1) {
return;
}
referenceCount--;
}
void UIStyle::PaintTextLayers(EsPainter *painter, EsTextPlan *plan, EsRectangle textBounds, const EsTextSelection *selectionProperties) {
EsBuffer data = {};
data.in = (uint8_t *) (this + 1);
data.bytes = layerDataByteCount;
EsTextStyle primaryStyle = TextPlanGetPrimaryStyle(plan);
bool restore = false;
for (uintptr_t i = 0; i < style->layerCount; i++) {
const ThemeLayer *layer = (const ThemeLayer *) EsBufferRead(&data, sizeof(ThemeLayer));
if (!layer) break;
if (layer->mode == THEME_LAYER_MODE_CONTENT && layer->type == THEME_LAYER_TEXT) {
EsBuffer data2 = data;
const ThemeLayerText *textLayer = (const ThemeLayerText *) EsBufferRead(&data2, sizeof(ThemeLayerText));
if (!textLayer) break;
EsTextStyle textStyle = primaryStyle;
textStyle.color = textLayer->color;
textStyle.blur = textLayer->blur;
EsTextPlanReplaceStyleRenderProperties(plan, &textStyle);
restore = true;
EsDrawText(painter, plan, Translate(textBounds, layer->offset.l, layer->offset.t));
}
EsBufferRead(&data, layer->dataByteCount - sizeof(ThemeLayer));
}
if (restore) EsTextPlanReplaceStyleRenderProperties(plan, &primaryStyle);
EsDrawText(painter, plan, textBounds, nullptr, selectionProperties);
}
void UIStyle::PaintText(EsPainter *painter, EsElement *element, EsRectangle rectangle,
const char *text, size_t textBytes, uint32_t iconID, uint32_t flags, const EsTextSelection *selectionProperties) {
EsTextSelection _selectionProperties;
EsRectangle bounds = Translate(EsRectangleAddBorder(rectangle, insets), painter->offsetX, painter->offsetY);
EsRectangle textBounds = bounds;
EsRectangle oldClip = painter->clip;
EsRectangleClip(painter->clip, Translate(rectangle, painter->offsetX, painter->offsetY), &painter->clip);
EsRectangle iconBounds = EsRectangleSplit(&textBounds, metrics->iconSize, metrics->layoutVertical ? 't' : 'l', gapMinor);
EsPainter iconPainter = *painter;
iconPainter.width = Width(iconBounds), iconPainter.height = Height(iconBounds);
iconPainter.offsetX = iconBounds.l, iconPainter.offsetY = iconBounds.t;
EsMessage m = { ES_MSG_PAINT_ICON };
m.painter = &iconPainter;
if (ES_HANDLED == EsMessageSend(element, &m)) {
// Icon painted by the application.
} else if (iconID) {
EsDrawStandardIcon(painter, iconID, metrics->iconSize, iconBounds, metrics->iconColor);
} else {
// Restore the previous bounds.
textBounds = bounds;
}
if (flags & (ES_DRAW_CONTENT_MARKER_DOWN_ARROW | ES_DRAW_CONTENT_MARKER_UP_ARROW)) {
EsStyleID part = (flags & ES_DRAW_CONTENT_MARKER_DOWN_ARROW) ? ES_STYLE_MARKER_DOWN_ARROW : ES_STYLE_MARKER_UP_ARROW;
UIStyle *style = GetStyle(MakeStyleKey(part, 0), true);
textBounds.r -= style->preferredWidth + gapMinor;
EsRectangle location = ES_RECT_4PD(bounds.r - style->preferredWidth - painter->offsetX,
bounds.t + Height(bounds) / 2 - style->preferredHeight / 2 - painter->offsetY,
style->preferredWidth, style->preferredHeight);
style->PaintLayers(painter, location, THEME_CHILD_TYPE_ONLY, THEME_LAYER_MODE_BACKGROUND);
}
if (textBytes == (size_t) -1) {
textBytes = EsCStringLength(text);
}
if (selectionProperties) {
_selectionProperties = *selectionProperties;
_selectionProperties.foreground = metrics->selectedText;
_selectionProperties.background = metrics->selectedBackground;
selectionProperties = &_selectionProperties;
}
if (textBytes) {
EsTextPlanProperties properties = {};
properties.flags = textAlign;
if (flags & ES_TEXT_H_LEFT) properties.flags = (properties.flags & ~(ES_TEXT_H_CENTER | ES_TEXT_H_RIGHT)) | ES_TEXT_H_LEFT;
if (flags & ES_TEXT_H_CENTER) properties.flags = (properties.flags & ~(ES_TEXT_H_LEFT | ES_TEXT_H_RIGHT)) | ES_TEXT_H_CENTER;
if (flags & ES_TEXT_H_RIGHT) properties.flags = (properties.flags & ~(ES_TEXT_H_LEFT | ES_TEXT_H_CENTER)) | ES_TEXT_H_RIGHT;
if (flags & ES_TEXT_V_TOP) properties.flags = (properties.flags & ~(ES_TEXT_V_CENTER | ES_TEXT_V_BOTTOM)) | ES_TEXT_V_TOP;
if (flags & ES_TEXT_V_CENTER) properties.flags = (properties.flags & ~(ES_TEXT_V_TOP | ES_TEXT_V_BOTTOM)) | ES_TEXT_V_CENTER;
if (flags & ES_TEXT_V_BOTTOM) properties.flags = (properties.flags & ~(ES_TEXT_V_TOP | ES_TEXT_V_CENTER)) | ES_TEXT_V_BOTTOM;
EsTextRun textRun[2] = {};
textRun[1].offset = textBytes;
GetTextStyle(&textRun[0].style);
if (flags & ES_DRAW_CONTENT_TABULAR) {
textRun[0].style.figures = ES_TEXT_FIGURE_TABULAR;
}
if (flags & ES_DRAW_CONTENT_RICH_TEXT) {
char *string;
EsTextRun *textRuns;
size_t textRunCount;
EsRichTextParse(text, textBytes, &string, &textRuns, &textRunCount, &textRun[0].style);
EsTextPlan *plan = EsTextPlanCreate(element, &properties, textBounds, string, textRuns, textRunCount);
if (plan) {
EsDrawText(painter, plan, textBounds, nullptr, selectionProperties);
EsTextPlanDestroy(plan);
}
EsHeapFree(textRuns);
EsHeapFree(string);
} else {
EsTextPlan *plan = EsTextPlanCreate(element, &properties, textBounds, text, textRun, 1);
if (plan) {
PaintTextLayers(painter, plan, textBounds, selectionProperties);
EsTextPlanDestroy(plan);
}
}
}
painter->clip = oldClip;
}
void EsDrawContent(EsPainter *painter, EsElement *element, EsRectangle rectangle,
const char *text, ptrdiff_t textBytes, uint32_t iconID, uint32_t flags, const EsTextSelection *selectionProperties) {
if (textBytes == -1) textBytes = EsCStringLength(text);
((UIStyle *) painter->style)->PaintText(painter, element, rectangle, text, textBytes, iconID, flags, selectionProperties);
}
void EsDrawTextLayers(EsPainter *painter, EsTextPlan *plan, EsRectangle bounds, const EsTextSelection *selectionProperties) {
((UIStyle *) painter->style)->PaintTextLayers(painter, plan, bounds, selectionProperties);
}
void UIStyle::PaintLayers(EsPainter *painter, EsRectangle location, int childType, int whichLayers) {
EsBuffer data = {};
data.in = (uint8_t *) (this + 1);
data.bytes = layerDataByteCount;
if (!THEME_RECT_VALID(painter->clip)) {
return;
}
EsRectangle opaqueRegion = {};
EsRectangle _bounds = Translate(location, painter->offsetX, painter->offsetY);
if (opaqueInsets.l != 0x7F && opaqueInsets.r != 0x7F
&& opaqueInsets.t != 0x7F && opaqueInsets.b != 0x7F) {
opaqueRegion = THEME_RECT_4(_bounds.l + opaqueInsets.l, _bounds.r - opaqueInsets.r,
_bounds.t + opaqueInsets.t, _bounds.b - opaqueInsets.b);
}
if (appearanceIndex != -1 && whichLayers == 0) {
EsThemeAppearance *themeAppearance = &theming.internedStyles[appearanceIndex].appearance;
EsDrawRectangle(painter, _bounds, themeAppearance->backgroundColor, themeAppearance->borderColor, themeAppearance->borderSize);
return;
}
for (uintptr_t i = 0; i < style->layerCount; i++) {
const ThemeLayer *layer = (const ThemeLayer *) EsBufferRead(&data, sizeof(ThemeLayer));
if (!layer) {
return;
}
EsRectangle bounds;
bounds.l = _bounds.l + (int) (scale * layer->offset.l) + THEME_RECT_WIDTH(_bounds) * layer->position.l / 100;
bounds.r = _bounds.l + (int) (scale * layer->offset.r) + THEME_RECT_WIDTH(_bounds) * layer->position.r / 100;
bounds.t = _bounds.t + (int) (scale * layer->offset.t) + THEME_RECT_HEIGHT(_bounds) * layer->position.t / 100;
bounds.b = _bounds.t + (int) (scale * layer->offset.b) + THEME_RECT_HEIGHT(_bounds) * layer->position.b / 100;
if (layer->mode == whichLayers) {
EsBuffer data2 = data;
if (layer->type == THEME_LAYER_BOX) {
ThemeDrawBox(painter, bounds, &data2, scale, layer, opaqueRegion, childType);
} else if (layer->type == THEME_LAYER_PATH) {
ThemeDrawPath(painter, bounds, &data2, scale);
}
}
EsBufferRead(&data, layer->dataByteCount - sizeof(ThemeLayer));
}
}
inline void UIStyle::GetTextStyle(EsTextStyle *style) {
// Also need to update PaintText.
EsMemoryZero(style, sizeof(EsTextStyle));
style->font = font;
style->size = metrics->textSize;
style->color = metrics->textColor;
style->figures = metrics->textFigures;
}
bool UIStyle::IsStateChangeObserved(uint16_t state1, uint16_t state2) {
if (((state1 & ~THEME_PRIMARY_STATE_MASK) ^ (state2 & ~THEME_PRIMARY_STATE_MASK)) & observedStyleStateMask) {
return true;
}
if (((0x10000 << (state1 & THEME_PRIMARY_STATE_MASK)) ^ (0x10000 << (state2 & THEME_PRIMARY_STATE_MASK))) & observedStyleStateMask) {
return true;
}
return false;
}
bool UIStyle::IsRegionCompletelyOpaque(EsRectangle region, int width, int height) {
return region.l >= opaqueInsets.l && region.r < width - opaqueInsets.r
&& region.t >= opaqueInsets.t && region.b < height - opaqueInsets.b;
}
void EsDrawRoundedRectangle(EsPainter *painter, EsRectangle bounds, EsDeviceColor mainColor, EsDeviceColor borderColor, EsRectangle borderSize, EsCornerRadii cornerRadii) {
ThemeLayer layer = {};
uint8_t info[sizeof(ThemeLayerBox) + sizeof(ThemePaintSolid) * 2] = {};
ThemeLayerBox *infoBox = (ThemeLayerBox *) info;
infoBox->borders = { (int8_t) borderSize.l, (int8_t) borderSize.r, (int8_t) borderSize.t, (int8_t) borderSize.b };
infoBox->corners = { (int8_t) cornerRadii.tl, (int8_t) cornerRadii.tr, (int8_t) cornerRadii.bl, (int8_t) cornerRadii.br };
infoBox->mainPaintType = THEME_PAINT_SOLID;
infoBox->borderPaintType = THEME_PAINT_SOLID;
ThemePaintSolid *infoMain = (ThemePaintSolid *) (infoBox + 1);
infoMain->color = mainColor;
ThemePaintSolid *infoBorder = (ThemePaintSolid *) (infoMain + 1);
infoBorder->color = borderColor;
EsBuffer data = { .in = (const uint8_t *) &info, .bytes = sizeof(info) };
ThemeDrawBox(painter, bounds, &data, 1, &layer, {}, THEME_CHILD_TYPE_ONLY);
}
#endif