Files
MeowOS-SDK/sdk/meow_ui_theme.c
T

410 lines
20 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;
/* §8.1.5 色板注入:一份自定义板覆写当前生效主题(src==NULL 恢复内建)。
* 覆写优先于 settings 解析(随 meow_theme_reload() 重读 settings 后仍生效,与 meow_ui_set_style
* 的「程序化覆写不清 settings」语义一致)。 */
static meow_theme g_inject;
static int g_inject_set = 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 (g_inject_set) return &g_inject; /* §8.1.5 注入板优先 */
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.5 色板导出/注入(单一权威桥)----
* 导出:把当前「style×mode」解析生效的完整权威主题拷给调用方(不取指针以防调用方误改内部表)。
* 注入:用一份自定义板覆写当前生效主题;NULL 恢复内建。二者都触发 load(g_loaded==0 时先读
* settings.conf),使宿主无 settings 文件(单测/脱机)也可靠。 */
void meow_theme_export(meow_theme *out)
{
if (!out) return;
const meow_theme *t = meow_theme_get(); /* 解析 g_mode(AUTO→明/暗)+ g_style + 背景覆写 */
*out = *t;
}
void meow_theme_inject(const meow_theme *src)
{
if (!g_loaded) { load_settings(); g_loaded = 1; }
if (!src) { g_inject_set = 0; broadcast(); return; }
g_inject = *src;
g_inject_set = 1;
broadcast(); /* 通知 App/状态栏/控件下一帧用新注入板重绘 */
}
/* ---- §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;
}
}
}