Files
MeowOS-SDK/meow_ui_theme.c
T
MeowOS 1664673238 M2.90: MIUIx 风格包落地 + ui.style 一键切换闭环
- meow_ui_theme.c: 为 MEOW_STYLE_MIUIX 实现整套 light/dark 配色(暖基面+小米橙 primary
  0xF57200/0xFF8C4A+蓝 tertiary;圆角 20/卡片 28、间距 12、细描边 1),与 MD3 可感知差异
- display.background 随 style×mode 派生 MIUIx 色板(bg_surfaces 增 style 参;图片背景保持各 style 默认面)
- meow_settings.c g_defs: ui.style 值域 'md3,miui'→'md3,miuix'(label Material You,MIUIx);
  apply_value SET_ENUM 对存量 'miui' 兼容映射为 miuix
- settings_page s_commit: ui.style 分支 → meow_ui_set_style + paint_sb_right + meow_dirty_full 即时换肤
- font_theme s_ui_theme() 接 ui.style + MIUIx dark/light 表;main_menu 背景签名含 ui.style + 纯色底随 style 换族
- 宿主编译 -Wall -Wextra 0 warning;test_meow_ui_theme 四套 s×m 渲染全 PASS;
  test_meow_ui_widgets/test_sdk_fixes 全 PASS(MD3 行为一致)
- 产物 md5: meow_ui.a a4231296 / launcher 9f97af1b(未部署走固件)/ hello e5527578 /
  files 993e41ca / ncm 6b9eeb0f(三 App 已 .new+mv 热更到卡, 设备 md5 已核一致)
- 待后续: 状态栏图标集仍 MD3 一套(sb_icons.h 无 per-style 换集接口, MIUIx 仅换配色);
  App 进程 SDK 主题缓存 g_loaded 不随 settings 外部改写重读, 运行中 App 下次启动才换肤
2026-08-31 20:17:51 +08:00

383 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* meow_ui_theme.c — 主题/配置(B1.5 / M2.46)。
*
* 内置日间/夜间两套色板;模式跟随系统设置键 display.theme_mode + display.theme
* (/usr/data/meow/settings.conf,与 launcher meow_settings 同源)。
* 判定与 launcher 的 s_ui_theme() 完全对齐(避免两套逻辑漂移):
* - display.theme_mode = manual:直读 display.theme(dark/light)决定深浅。
* - display.theme_mode = auto:按深色时段 display.theme_dark_start/end(分钟 0-1439),
* 半开区间 [start,end);start>end 跨午夜;start==end 视为未配置 → 回退 manual(display.theme)。
* 读取缺失/非法一律回退深色(与 launcher 默认一致)。
* 切换即广播 listener。AUTO 解析只在查询时按当前系统时间计算(不缓存)。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_theme_reload() 定义(供 launcher 逼重读 settings) */
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <time.h>
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
/* ---- 主题表(§5.8.4 + §8.1.3):[style][dark] ———
* 0=MEOW_STYLE_MD3, 1=MEOW_STYLE_MIUIX;dark 0=light, 1=dark。
* 旧字段 bg/fg/accent/dim = surface/onSurface/primary/onSurfaceVariant(映射见 meow_ui.h)。
* bg/surface 随后由「背景全局源」(display.background,§8.1.4)按 style×mode 覆写。 */
static meow_theme g_theme[2][2];
static meow_theme_mode g_mode = MEOW_THEME_DARK; /* 配置生效的判定模式(默认深色,与 launcher 默认一致) */
static meow_theme_mode g_manual = MEOW_THEME_DARK; /* display.theme 解析出的手动深浅;auto 未配置时段时回退用之 */
static int g_dark_start = 0; /* 深色时段开始(分钟 0-1439) */
static int g_dark_end = 0; /* 深色时段结束(分钟 0-1439) */
static int g_loaded = 0;
static int g_style = MEOW_STYLE_MD3; /* 顶级主题风格族(§8.1.2):默认 MD3 */
static char g_background[64] = ""; /* display.background(black/blue/green/white/… 或图片路径) */
static meow_theme_listener g_listeners[8];
static int g_nlisteners = 0;
/* ---- 读 /usr/data/meow/settings.conf 的 key=value(兼容 "key = value" 带空格形式)----
* 与 launcher meow_settings_import 同款解析:按 '=' 切分,key/value 各自去首尾空白,
* 精确匹配 key(避免 display.theme 误匹配 display.theme_mode)。返回 0=命中,-1=未命中。 */
static int get_setting_str(const char *key, char *buf, int buflen)
{
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
if (!f || buflen <= 0) return -1;
char line[160];
int kl = (int)strlen(key);
int found = 0;
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue;
/* key = 行首到 '=',去尾空白 */
char *k = line;
while (*k == ' ' || *k == '\t') k++;
int n = (int)(eq - k);
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--;
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue;
/* value = '=' 后,去首尾空白 / 行尾 CRLF */
const char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++;
int m = 0;
while (*v && *v != '\n' && *v != '\r' && m < buflen - 1) buf[m++] = *v++;
while (m > 0 && (buf[m-1] == ' ' || buf[m-1] == '\t')) m--;
buf[m] = 0;
found = 1;
break;
}
fclose(f);
return found ? 0 : -1;
}
static int get_setting_int(const char *key, int *out)
{
char buf[32];
if (get_setting_str(key, buf, sizeof buf) != 0) return -1;
*out = atoi(buf);
return 0;
}
/* 解析 display.theme 的值(dark/light,非索引)→ LIGHT/DARK;未知/缺失回退 DARK(launcher 默认)。 */
static meow_theme_mode parse_manual(const char *v)
{
while (*v == ' ' || *v == '\t') v++;
if (!strncmp(v, "light", 5)) return MEOW_THEME_LIGHT;
return MEOW_THEME_DARK; /* "dark" / 未知 → 深色 */
}
/* ---- 填充 4 张主题表的内建色板(§8.1.3:MD3 / MIUIx 各一整套,light/dark 各一)----
* bg/surface 本函数填默认面;随后 apply_background() 按 display.background 按 style 覆写。 */
static void init_defaults(void)
{
meow_theme *t;
/* MD3 light */
t = &g_theme[MEOW_STYLE_MD3][0];
t->bg = 0xE8EAF0; t->fg = 0x1A1A2A; t->accent = 0x2A5A8A; t->dim = 0x4A4F5A;
t->font_title = 28; t->font_body = 20;
t->primary = 0x2A5A8A; t->onPrimary = 0xFFFFFF; t->primaryContainer = 0xC9E2F5; t->onPrimaryContainer = 0x0A2B46;
t->secondary = 0x53626E; t->onSecondary = 0xFFFFFF; t->secondaryContainer = 0xD6E5F2; t->onSecondaryContainer = 0x101E28;
t->tertiary = 0x635B78; t->onTertiary = 0xFFFFFF; t->tertiaryContainer = 0xE9DEFB; t->onTertiaryContainer = 0x1E1734;
t->surface = 0xE8EAF0; t->onSurface = 0x1A1A2A; t->surfaceVariant = 0xDDE1EA; t->onSurfaceVariant = 0x4A4F5A;
t->surfaceContainerLowest = 0xFFFFFF; t->surfaceContainerLow = 0xF2F3F7; t->surfaceContainer = 0xECEEF3;
t->surfaceContainerHigh = 0xE6E8EE; t->surfaceContainerHighest = 0xE1E3E9;
t->surfaceDim = 0xD8DAE0; t->surfaceBright = 0xFFFFFF; t->surfaceTint = 0x2A5A8A;
t->outline = 0x74777F; t->outlineVariant = 0xC4C6D0; t->scrim = 0x000000;
t->error = 0xBA1A1A; t->onError = 0xFFFFFF; t->errorContainer = 0xFFDAD6; t->onErrorContainer = 0x410002;
t->inverseSurface = 0x2F3036; t->inverseOnSurface = 0xF0F0F5; t->inversePrimary = 0xA6C8EE;
t->shadow = 0x000000;
t->radius = 12; t->radius_card = 16; t->spacing = 8; t->stroke_w = 0;
/* MD3 dark */
t = &g_theme[MEOW_STYLE_MD3][1];
t->bg = 0x141824; t->fg = 0xFFFFFF; t->accent = 0x4A6A9A; t->dim = 0x9AA4C8;
t->font_title = 28; t->font_body = 20;
t->primary = 0x4A6A9A; t->onPrimary = 0xFFFFFF; t->primaryContainer = 0x2A3A5C; t->onPrimaryContainer = 0xD5E4F5;
t->secondary = 0xB8C6D2; t->onSecondary = 0x27323B; t->secondaryContainer = 0x3A4650; t->onSecondaryContainer = 0xD6E5F2;
t->tertiary = 0xCDC2E6; t->onTertiary = 0x372C52; t->tertiaryContainer = 0x4E4269; t->onTertiaryContainer = 0xE9DEFB;
t->surface = 0x141824; t->onSurface = 0xFFFFFF; t->surfaceVariant = 0x2A2F3C; t->onSurfaceVariant = 0x9AA4C8;
t->surfaceContainerLowest = 0x0F121C; t->surfaceContainerLow = 0x1C2030; t->surfaceContainer = 0x20253A;
t->surfaceContainerHigh = 0x262C44; t->surfaceContainerHighest = 0x2C334E;
t->surfaceDim = 0x12151E; t->surfaceBright = 0x3A3E50; t->surfaceTint = 0x4A6A9A;
t->outline = 0x8B93A3; t->outlineVariant = 0x3E4450; t->scrim = 0x000000;
t->error = 0xFFB4AB; t->onError = 0x690005; t->errorContainer = 0x93000A; t->onErrorContainer = 0xFFDAD6;
t->inverseSurface = 0xF0F0F5; t->inverseOnSurface = 0x2F3036; t->inversePrimary = 0xA6C8EE;
t->shadow = 0x000000;
t->radius = 12; t->radius_card = 16; t->spacing = 8; t->stroke_w = 0;
/* MIUIx(§8.1.3 定案):完整独立色板(不复刻 MD3)——暖色基面 + 小米橙 primary +
* 更大圆角/更松间距/细描边。观感:MIUI 系暖白(浅)/暖黑(深)、橙强调、蓝作 tertiary 辅助。
* 与 MD3 保持可感知差异(圆角/描边/间距/主色),避免"切了没感觉"。 */
t = &g_theme[MEOW_STYLE_MIUIX][0]; /* MIUIx light */
t->bg = 0xF7F3EE; t->fg = 0x251A10; t->accent = 0xF57200; t->dim = 0x7D6A5A;
t->font_title = 28; t->font_body = 20;
t->primary = 0xF57200; t->onPrimary = 0xFFFFFF; t->primaryContainer = 0xFFE2CC; t->onPrimaryContainer = 0x5E2B00;
t->secondary = 0x8A6F5C; t->onSecondary = 0xFFFFFF; t->secondaryContainer = 0xF0E2D6; t->onSecondaryContainer = 0x3A2A1C;
t->tertiary = 0x3A5FA8; t->onTertiary = 0xFFFFFF; t->tertiaryContainer = 0xD6E2FA; t->onTertiaryContainer = 0x1E2C4A;
t->surface = 0xF7F3EE; t->onSurface = 0x251A10; t->surfaceVariant = 0xEDE2D7; t->onSurfaceVariant = 0x7D6A5A;
t->surfaceContainerLowest = 0xFFFFFF; t->surfaceContainerLow = 0xFAF6F1; t->surfaceContainer = 0xF4EDE3;
t->surfaceContainerHigh = 0xEEE6D9; t->surfaceContainerHighest = 0xE9DFD0;
t->surfaceDim = 0xDFD3C4; t->surfaceBright = 0xFFFBF6; t->surfaceTint = 0xF57200;
t->outline = 0x85746A; t->outlineVariant = 0xCBBFB2; t->scrim = 0x000000;
t->error = 0xB3261E; t->onError = 0xFFFFFF; t->errorContainer = 0xFFDAD6; t->onErrorContainer = 0x410002;
t->inverseSurface = 0x322A22; t->inverseOnSurface = 0xF6EFE6; t->inversePrimary = 0xFFB58C;
t->shadow = 0x000000;
t->radius = 20; t->radius_card = 28; t->spacing = 12; t->stroke_w = 1;
t = &g_theme[MEOW_STYLE_MIUIX][1]; /* MIUIx dark */
t->bg = 0x181310; t->fg = 0xEDE5DC; t->accent = 0xFF8C4A; t->dim = 0xAB9884;
t->font_title = 28; t->font_body = 20;
t->primary = 0xFF8C4A; t->onPrimary = 0x241100; t->primaryContainer = 0x5E2B00; t->onPrimaryContainer = 0xFFDDC8;
t->secondary = 0xC9B5A2; t->onSecondary = 0x34261A; t->secondaryContainer = 0x4A3A2C; t->onSecondaryContainer = 0xEADCCD;
t->tertiary = 0x9EB8EC; t->onTertiary = 0x1E2C4A; t->tertiaryContainer = 0x2E4370; t->onTertiaryContainer = 0xD6E2FA;
t->surface = 0x181310; t->onSurface = 0xEDE5DC; t->surfaceVariant = 0x332B25; t->onSurfaceVariant = 0xAB9884;
t->surfaceContainerLowest = 0x120E0B; t->surfaceContainerLow = 0x211B16; t->surfaceContainer = 0x262019;
t->surfaceContainerHigh = 0x302922; t->surfaceContainerHighest = 0x3A3229;
t->surfaceDim = 0x120E0B; t->surfaceBright = 0x3F372E; t->surfaceTint = 0xFF8C4A;
t->outline = 0x8A7B6B; t->outlineVariant = 0x4A3F35; t->scrim = 0x000000;
t->error = 0xFFB4AB; t->onError = 0x690005; t->errorContainer = 0x93000A; t->onErrorContainer = 0xFFDAD6;
t->inverseSurface = 0xEDE5DC; t->inverseOnSurface = 0x322A22; t->inversePrimary = 0xF57200;
t->shadow = 0x000000;
t->radius = 20; t->radius_card = 28; t->spacing = 12; t->stroke_w = 1;
}
/* ---- 背景全局源(§8.1.4):display.background 是「style×mode 下背景底选项集」的当前值,
* 决定主题 surface/bg(深浅成对),且随 style 换族(§8.1.1 矩阵 MD3/MIUIx 各一套)。
* 按名映射 (style, dark_surface, light_surface):
* MD3:dark 默认/black → 0x141824 / light 默认/white → 0xE8EAF0(既有默认,App 无回归);
* blue → 0x0A1428 / 0xE5EEFB;green → 0x0A2814 / 0xE6F6E8。
* MIUIx:dark 默认/black → 0x181310 / light 默认/white → 0xF7F3EE(暖色族);
* blue → 0x101A2E / 0xEDEAF5;green → 0x14251A / 0xEFF3EC。
* 图片路径(*.png/jpg/jpeg):背景图即观感 → 用该 style 的默认 surface(本批草案)。 */
static int bg_is_image(const char *v)
{
while (*v == ' ' || *v == '\t') v++;
const char *dot = strrchr(v, '.');
if (!dot || dot == v) return 0;
return !strncasecmp(dot, ".png", 4) || !strncasecmp(dot, ".jpg", 4) || !strncasecmp(dot, ".jpeg", 5);
}
static void bg_surfaces(int style, const char *v, uint32_t *dark, uint32_t *light)
{
/* 默认面(未知/图片/空)→ 各 style 的默认 surface(MIUIx 暖色族) */
if (style == MEOW_STYLE_MIUIX) { *dark = 0x181310; *light = 0xF7F3EE; }
else { *dark = 0x141824; *light = 0xE8EAF0; }
while (*v == ' ' || *v == '\t') v++;
if (bg_is_image(v)) return; /* 背景图:用该 style 默认面 */
if (style == MEOW_STYLE_MIUIX) {
if (!strncmp(v, "blue", 4)) { *dark = 0x101A2E; *light = 0xEDEAF5; return; }
if (!strncmp(v, "green", 5)) { *dark = 0x14251A; *light = 0xEFF3EC; return; }
if (!strncmp(v, "white", 5)) { *dark = 0x181310; *light = 0xFFFFFF; return; }
if (!strncmp(v, "lightblue", 9)) { *dark = 0x101A2E; *light = 0xEDEAF5; return; }
if (!strncmp(v, "lightgreen", 10)) { *dark = 0x14251A; *light = 0xEFF3EC; return; }
return; /* black / 其它未知 → MIUIx 默认暖面 */
}
/* MD3(既有语义,保持不变) */
if (!strncmp(v, "blue", 4)) { *dark = 0x0A1428; *light = 0xE5EEFB; return; }
if (!strncmp(v, "green", 5)) { *dark = 0x0A2814; *light = 0xE6F6E8; return; }
if (!strncmp(v, "white", 5)) { *dark = 0x141824; *light = 0xFFFFFF; return; }
if (!strncmp(v, "lightblue", 9)) { *dark = 0x0A1428; *light = 0xE5EEFB; return; }
if (!strncmp(v, "lightgreen", 10)) { *dark = 0x0A2814; *light = 0xE6F6E8; return; }
/* black / 其它未知 → 默认面 */
}
static void apply_background(void)
{
uint32_t ds, ls;
for (int s = 0; s < 2; s++) {
/* 图片背景:底色即观感 → 保持 init_defaults 各 style 的默认 surface(不覆写) */
if (bg_is_image(g_background)) continue;
bg_surfaces(s, g_background, &ds, &ls);
g_theme[s][0].bg = g_theme[s][0].surface = ls; /* light 行 */
g_theme[s][1].bg = g_theme[s][1].surface = ds; /* dark 行 */
}
}
static void load_settings(void)
{
char buf[64];
g_manual = MEOW_THEME_DARK;
g_mode = MEOW_THEME_DARK;
g_dark_start = 0;
g_dark_end = 0;
/* display.theme:手动深浅(auto 未配置时段时也回退这里) */
if (get_setting_str("display.theme", buf, sizeof buf) == 0)
g_manual = parse_manual(buf);
/* display.theme_mode:manual=读 display.theme;auto=时段。缺失/非法 → manual */
if (get_setting_str("display.theme_mode", buf, sizeof buf) == 0) {
if (!strcmp(buf, "auto")) g_mode = MEOW_THEME_AUTO;
else g_mode = g_manual; /* manual(含 "light"/"dark"/未知)→ 读 display.theme */
} else {
g_mode = g_manual; /* 无 theme_mode 键 → 默认 manual → display.theme */
}
get_setting_int("display.theme_dark_start", &g_dark_start);
get_setting_int("display.theme_dark_end", &g_dark_end);
/* 顶级主题风格族(§8.1.2 minimal):ui.style 的 md3/miuix。读取缺失/未知 → 回退 MD3。
* 决定状态栏右侧图标集(§3.1 同一个状态栏);配色日/夜仍由 display.theme 现有解析管。 */
g_style = MEOW_STYLE_MD3;
if (get_setting_str("ui.style", buf, sizeof buf) == 0) {
const char *v = buf;
while (*v == ' ' || *v == '\t') v++;
if (!strncmp(v, "miuix", 5) || !strncmp(v, "miui", 4)) g_style = MEOW_STYLE_MIUIX;
}
/* 背景全局源(§8.1.4):display.background 为 style×mode 下背景底选项集的当前值。
* 读取缺失/未知 → ""(默认面)。 */
g_background[0] = 0;
if (get_setting_str("display.background", g_background, sizeof g_background) != 0)
g_background[0] = 0;
/* 载入/覆写 4 张主题表(色板 + style×mode 背景 surface/bg) */
init_defaults();
apply_background();
}
/* 当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用(与 launcher s_now_min_of_day 一致)。 */
static int now_min_of_day(void)
{
time_t t = time(NULL);
struct tm tm;
localtime_r(&t, &tm);
return tm.tm_hour * 60 + tm.tm_min;
}
/* 深色时段判定(与 launcher theme_should_dark 完全一致):now_min ∈ [start,end) → 深色。
* - start==end 视为未配置,返回 -1(由调用方决定回退策略)。
* - start<end:非跨天(如 720→1080 = 12:00→18:00)。
* - start>end:跨午夜(如 1140→420 = 19:00→次日07:00)。
* - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */
static int theme_should_dark(int now_min, int start, int end)
{
if (start == end) return -1; /* 未配置 */
if (start < end)
return (now_min >= start && now_min < end) ? 1 : 0;
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
}
static meow_theme_mode resolve_mode(meow_theme_mode m)
{
if (m != MEOW_THEME_AUTO) return m;
int d = theme_should_dark(now_min_of_day(), g_dark_start, g_dark_end);
if (d < 0) return g_manual; /* 时段未配置:回退 display.theme 手动深浅 */
return d ? MEOW_THEME_DARK : MEOW_THEME_LIGHT;
}
static void broadcast(void)
{
for (int i = 0; i < g_nlisteners; i++)
if (g_listeners[i]) g_listeners[i](g_mode);
}
/* 当前「style×mode」主题:style=0/1(MD3/MIUIX),dark=0(light)/1(dark)。 */
static const meow_theme *theme_current(int style, int dark)
{
if (style < MEOW_STYLE_MD3) style = MEOW_STYLE_MD3;
if (style > MEOW_STYLE_MIUIX) style = MEOW_STYLE_MIUIX;
return &g_theme[style][dark ? 1 : 0];
}
const meow_theme *meow_theme_get(void)
{
if (!g_loaded) {
load_settings();
g_loaded = 1;
}
return theme_current(g_style, resolve_mode(g_mode) == MEOW_THEME_DARK);
}
meow_theme_mode meow_theme_get_mode(void)
{
if (!g_loaded) { load_settings(); g_loaded = 1; }
return g_mode;
}
void meow_theme_set_mode(meow_theme_mode m)
{
if (!g_loaded) { load_settings(); g_loaded = 1; }
if (m < MEOW_THEME_LIGHT || m > MEOW_THEME_AUTO) return;
if (m == g_mode) return;
g_mode = m;
broadcast();
}
/* 重置缓存:下一次 meow_theme_get()/get_mode()/defaults() 重读 settings.conf。
* launcher 在 statusbar 刷新前调用,让 SDK 全局状态栏(其主题缓存于 g_loaded)跟手
* launcher 侧主题变更(settings.conf 被改后立即反映),避免「界面已换肤、状态栏滞留旧主题」。 */
void meow_theme_reload(void)
{
g_loaded = 0;
}
meow_theme meow_theme_defaults(meow_theme_mode m)
{
if (!g_loaded) { load_settings(); g_loaded = 1; }
/* 脱机取内置板:返回 MD3(默认)style 下解析出的模式表。 */
return g_theme[MEOW_STYLE_MD3][resolve_mode(m) == MEOW_THEME_DARK ? 1 : 0];
}
/* ---- §8.1.2 顶级主题风格族(minimal)----
* meow_ui_style() 返回当前 ui.style(读取走 load_settings,随 meow_theme_reload() 重读,
* 故 launcher 改 settings.conf 后经 sb_sdk_refresh→theme_reload 即跟手)。
* meow_ui_set_style() 记录 style(越界 clamp;同值忽略)并触发 reload/broadcast——供需要
* 显式切换的调用方(后续 launcher 设置页切换可改走此路径);状态栏图标集据此换集。 */
int meow_ui_style(void)
{
if (!g_loaded) { load_settings(); g_loaded = 1; }
return g_style;
}
void meow_ui_set_style(int style)
{
if (style < MEOW_STYLE_MD3) style = MEOW_STYLE_MD3;
if (style > MEOW_STYLE_MIUIX) style = MEOW_STYLE_MIUIX;
if (style == g_style) return;
g_style = style;
/* 触发下一帧按新 style 重绘(§8.1.2):g_theme[s][*] 是独立表(不随 g_style 重算),
* g_style 记录后 meow_theme_get() 立即可取新 style×mode 表;只需广播主题 listener,
* 让 App/状态栏/控件下一帧用新 style 重绘/换图标集。不清 settings 缓存(避免覆盖程序化 style)。 */
broadcast();
}
void meow_theme_add_listener(meow_theme_listener cb)
{
if (!cb) return;
if (g_nlisteners >= (int)(sizeof g_listeners / sizeof g_listeners[0])) return;
for (int i = 0; i < g_nlisteners; i++) if (g_listeners[i] == cb) return;
g_listeners[g_nlisteners++] = cb;
}
void meow_theme_remove_listener(meow_theme_listener cb)
{
for (int i = 0; i < g_nlisteners; i++) {
if (g_listeners[i] == cb) {
g_listeners[i] = g_listeners[g_nlisteners - 1];
g_nlisteners--;
return;
}
}
}