/* settings_page.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" /* SC_PRESET 预设表(路径/枚举字符串的"预设项,自定义文件选择留后期") */ static const char *const bg_presets[] = { "black", "blue", "green" }; static const char *const bg_preset_labels[] = { "黑色", "深蓝", "深绿" }; static const char *const hk_presets[] = { "power_voldown", "seq" }; static const char *const hk_preset_labels[] = { "电源+音量减", "连按序列" }; static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" }; static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" }; /* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区 * (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由 * s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */ static const char *const out_presets[] = { "hw:0,0" }; static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" }; /* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s, * 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按 * 45s/分/刻/时 跳档,最高 2 时。滑条值 = 档位索引(0..S_TIME_TIERS_N-1,步进 1), * 落盘仍写「秒」(display.screen_off_timeout);索引<->秒双向映射见 s_time_tier_*。 */ static const int s_time_tiers[] = { 0, /* 常亮 */ 5, 10, 15, 20, /* 细粒度段:可精确到 30s 附近 */ 30, 45, /* 半分钟/三刻钟 */ 60, 90, 120, 180, /* 1 分 / 1.5 分 / 2 分 / 3 分 */ 300, 600, 900, /* 5 分 / 10 分 / 15 分 */ 1800, /* 30 分 */ 3600, 7200 /* 1 时 / 2 时 */ }; /* 设置页扁平化菜单(分组=组标题行,其余=设置项)。顺序即显示顺序。 */ /* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */ static const srow_t SMENU[] = { /* 显示 */ { "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0,0 }, { "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, { "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0,0 }, { "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, { "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动), * 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */ { "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, { "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, { "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0,0 }, { "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0,0 }, { "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N,0 }, /* M2.79 主界面背景图片叠加层强度(图片背景上叠 黑/白 半透明遮罩,防文字/控件被图片淹没) */ { "背景叠加强度", "display.bg_overlay_alpha", SC_SLIDER, 0,255,1, NULL,NULL,0,0 }, /* M2.79 主界面布局:卡片宫格 / 列表(写 ui.home_layout,回主界面即生效) */ { "主界面布局", "ui.home_layout", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, /* 网络:M2.76 WiFi/蓝牙升级为独立子页面(安卓风)。入口行为「列表行」+右侧 chevron, * 摘要显示当前状态(s_net_get);点开进入对应子页(顶栏+返回+分组列表),详见 * wifi_page.c / bt_page.c。原来散落的开关/列表/信息行收敛进子页面。 */ { "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_PAGE }, { "蓝牙", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_PAGE }, /* 电源 */ { "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0,0 }, { "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0,0 }, { "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N,0 }, /* 开机 */ { "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, { "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, { "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0,0 }, /* 输入 */ { "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N,0 }, { "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0,0 }, { "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0,0 }, { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* 字体主题 */ { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, /* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。 * 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); * 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */ { "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/// 列表; * 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */ { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, { "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, { "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, { "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, { "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, { "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, { "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0,0 }, { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0,0 }, { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, /* 通知 */ { "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, { "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0,0 }, /* 日志 */ { "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, { "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* 音频 */ { "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0,0 }, { "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0,0 }, { "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0,0 }, { "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N,0 }, /* M6.x 锁屏:安全组「锁屏 PIN」独立子页面(状态/设置/修改/清除/唤醒锁开关,见 lock_settings.c) */ { "安全", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "锁屏 PIN", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_PIN_PAGE }, /* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */ { "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT }, }; /* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由 * 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */ #if 0 /* M7.2 回退位:旧「当前选中滑条行 track y」随旧手绘 draw_settings_page 退役 * (新路径在触摸按下时由 meow_listview hit 直接算 track 中心,无需全局记录)。 */ /* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量, * 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */ static int s_sel_track_y = -1; #endif /* ---- 行高/位置/导航辅助 ---- */ /* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。 * 可见性与分组的标题行无关(标题恒显示)。 */ static int s_row_visible(int i) { const srow_t *r = &SMENU[i]; if (r->key && !strncmp(r->key, "font.class_", 11)) { int on = 0; meow_settings_get_switch("display.multi_font", &on); return on; } return 1; } /* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */ static int s_vis_count(void) { int n = 0; for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++; return n; } /* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */ static int s_vis_of(int i) { if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1; int p = 0; for (int k = 0; k < i; k++) if (s_row_visible(k)) p++; return p; } /* 压缩位置 → SMENU 下标 */ static int s_vis_at(int p) { int c = 0; for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; } return SMENU_N - 1; } static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); } static int s_first_setting(void) { for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i; return 0; } static int s_next_setting(int i) { for (int k = i + 1; k < SMENU_N; k++) if (s_setting_idx(k)) return k; return s_first_setting(); /* 回绕 */ } /* 字体行(font.file + font.class_*)走「family→字重」两级子菜单(M2.6 字重结构) */ static int s_row_is_font(const srow_t *r) { return r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11)); } /* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组 * (TF 卡 MeowOS/fonts/// 扫描结果,见字体区 font_presets/font_presets_n), * 其余行用编译期静态表(r->presets / r->preset_n)。 */ static const char *const *s_row_presets(const srow_t *r, int *n) { if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) { if (n) *n = font_presets_n; return font_presets; } if (n) *n = r->preset_n; return r->presets; } #if 0 /* M7.2 回退位:旧「纯范围/选中可见滚动钳制 + 内容 y→行号」随旧手绘 draw_settings_page 退役 * (新路径滚动走 meow_scroll 包裹 meow_listview,钳制/命中/可见性由 SDK 接管)。 * 删除 #if 0 行并切回旧 draw_settings_page 路径即可恢复。 */ /* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */ static int s_clamp_scroll_range(int scroll) { int total = s_vis_count() * SROW_H; if (total <= SET_CONTENT_H) return 0; int max_scroll = total - SET_CONTENT_H; if (scroll < 0) scroll = 0; if (scroll > max_scroll) scroll = max_scroll; return scroll; } /* 滚动:保证当前选中行在 [SET_CONTENT_Y, SET_CONTENT_BOT] 内 */ static int s_clamp_scroll(int sel, int scroll) { scroll = s_clamp_scroll_range(scroll); int pos = s_vis_of(sel); if (pos < 0) pos = s_vis_of(s_first_setting()); int top = pos * SROW_H; int bot = top + SROW_H; if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */ if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */ return s_clamp_scroll_range(scroll); } /* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */ static int s_row_at_content_y(int cy) { if (cy < 0) return -1; int pos = cy / SROW_H; if (pos >= s_vis_count()) return -1; return s_vis_at(pos); } #endif /* M7.2 回退位:旧滚动钳制 + 内容 y→行号 */ /* ---- 值格式化/展示 ---- */ static void s_fmt_time(int sec, char *buf, int len) { if (sec <= 0) { snprintf(buf, len, "常亮"); return; } if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; } if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; } snprintf(buf, len, "%d 时", sec / 3600); } /* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */ static void s_fmt_minutes(int mins, char *buf, int len) { if (mins < 0) mins = 0; if (mins > 1439) mins = 1439; snprintf(buf, len, "%02d:%02d", mins / 60, mins % 60); } /* 息屏时间:秒 → 最近档位索引(显示滑动位置用;等值优先,跨档取较近档,平手取高档) */ static int s_time_tier_idx(int sec) { if (sec <= s_time_tiers[0]) return 0; for (int i = 1; i < S_TIME_TIERS_N; i++) { if (sec == s_time_tiers[i]) return i; if (sec < s_time_tiers[i]) { int lo = s_time_tiers[i - 1], hi = s_time_tiers[i]; return (sec - lo < hi - sec) ? i - 1 : i; } } return S_TIME_TIERS_N - 1; } /* 息屏时间:档位索引 → 秒(落盘写给 display.screen_off_timeout 的实值) */ static int s_time_tier_val(int idx) { if (idx < 0) idx = 0; if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1; return s_time_tiers[idx]; } static void s_fmt_value(const srow_t *r, char *buf, int len) { int v = 0; if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; } if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; } /* B2b 深色时段(分钟 0-1439 → "HH:MM");start==end 视为未配置,显示「未设置」 */ if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { int s2 = 0, e2 = 0; meow_settings_get_int("display.theme_dark_start", &s2); meow_settings_get_int("display.theme_dark_end", &e2); if (s2 == e2) { snprintf(buf, len, "未设置"); return; } s_fmt_minutes(v, buf, len); return; } /* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */ if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) { if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000); else snprintf(buf, len, "%.1f 秒", v / 1000.0); return; } snprintf(buf, len, "%d", v); } static void s_display_value(const srow_t *r, char *buf, int len) { if (r->ctrl == SC_ENUM) { int idx = 0; if (meow_settings_get_enum(r->key, &idx) != 0) idx = 0; if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-"); return; } /* SC_PRESET */ char cur[MEOW_SET_MAX_LEN]; if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; } /* M2.5x 多字体:class 行空值 = 跟随全局 font.file */ if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) { snprintf(buf, len, "跟随全局"); return; } int pn = 0; const char *const *pps = s_row_presets(r, &pn); for (int i = 0; i < pn; i++) { if (!strcmp(pps[i], cur)) { snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur); return; } } snprintf(buf, len, "%s", cur); } /* ---- 控件绘制(MD3 形态;全部用主题角色色,无散落硬编码)---- */ void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th) { int w = 48, h = 28; meow_gfx_fill_round(x, y, w, h, h / 2, on ? th->primary : th->surface_container); int knob_r = h / 2 - 3; int kx = on ? x + w - knob_r * 2 - 3 : x + 3; meow_gfx_fill_round(kx, y + 3, knob_r * 2, knob_r * 2, knob_r, on ? 0xFFFFFF : th->on_surface_variant); } void s_draw_slider(int x, int y, int w, int val, int mn, int mx, const meow_ui_theme_t *th) { int h = 6, ty = y; fill_rect(x, ty, w, h, th->surface_container); /* 未达段 */ int range = mx - mn; int frac = range ? (val - mn) * w / range : 0; if (frac > 0) fill_rect(x, ty, frac, h, th->primary); /* 已达段 */ int tx = x + frac, cy = ty + h / 2; meow_gfx_fill_round(tx - 12, cy - 12, 24, 24, 12, th->primary); /* thumb */ meow_gfx_fill_round(tx - 5, cy - 5, 10, 10, 5, 0xFFFFFF); /* thumb 中点 */ } /* ---- set 后统一侧效应(DAC/背光/刷新率)+ 可选落盘 ---- */ static void s_commit(const srow_t *r, int save_now) { if (!r || !r->key) return; if (!strcmp(r->key, "audio.volume")) { /* M2.38 Bug1/2:音量变更——拖动中节流写 DAC(防 system() 高频卡死),抬手才无条件直写; * 同时把 g_sys.vol 同步为当前值并刷整条状态栏(顶层 VOL 数字随之更新,不整屏重绘)。 */ g_sys.vol = settings_volume_get(); paint_sb_right(); if (save_now) meow_dac_vol_flush(); /* 抬手/点击落盘=无条件直写真实 DAC */ else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */ } else if (!strcmp(r->key, "display.brightness")) { /* M2.46-2 ①:亮度滑条拖动时状态栏百分比数字实时跟。根因:旧代码只 write_brightness(v) * 写背光,未同步 g_sys.brightness(状态栏亮度数字取它)也未重绘 → 数字卡住。 * 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。 * 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */ int v; if (meow_settings_get_int("display.brightness", &v) == 0) { write_brightness(v, "settings_page bright_slider"); g_sys.brightness = v; paint_sb_right(); } } else if (!strcmp(r->key, "display.refresh_rate")) meow_ui_set_refresh_rate(refresh_rate_cfg); else if (!strcmp(r->key, "display.refresh_always_max")) meow_ui_set_refresh_always_max(refresh_always_max); else if (!strcmp(r->key, "network.wifi_enabled")) { int on = 0; meow_settings_get_bool(r->key, &on); s_net_toggle_wifi(on); s_net_refresh(); } else if (!strcmp(r->key, "network.bluetooth_enabled")) { int on = 0; meow_settings_get_bool(r->key, &on); s_net_toggle_bt(on); s_net_refresh(); } /* M2.44 背景选项真正生效:display.background(black/blue/green) → menu_bg(主界面背景)。 * 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */ else if (!strcmp(r->key, "display.background")) { char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg); int mstyle = 0; if (meow_settings_get_enum("ui.style", &mstyle) != 0) mstyle = 0; if (mstyle == MEOW_STYLE_MIUIX) { /* MIUIx 暖色族(与 main_menu menu_bg_prepare 同色) */ if (!strcmp(bg, "blue")) menu_bg = 0x101A2E; else if (!strcmp(bg, "green")) menu_bg = 0x14251A; else menu_bg = 0x181310; } else { /* MD3(既有) */ if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */ else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */ else menu_bg = 0x000000; /* black(默认) */ } /* menu_bg 仅主界面背景用(状态栏走 SDK meow_theme bg);退出设置页回主界面时经 * menu_bg_sig(含 ui.style)重build 缓存。 */ } /* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */ else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") || !strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */ } /* §8.1.2 一键切换闭环:ui.style(md3/miuix)顶级风格族切换 → 全系统即时换肤,无需重启。 * meow_ui_set_style() 设 SDK g_style(launcher 同一进程内立即生效)+ 广播(App/状态栏下一帧换肤); * paint_sb_right() → sb_sdk_refresh() 逼 SDK 状态栏重读 settings(含 display.background 派生)跟手换肤; * meow_dirty_full() 让 launcher 主界面/设置页整帧重绘(s_ui_theme() 已接 ui.style → 换族配色)。 * 主界面背景缓存经 menu_bg_sig(含 ui.style,见 main_menu.c)在回主界面时自动重建。 */ else if (!strcmp(r->key, "ui.style")) { int st = 0; meow_settings_get_enum("ui.style", &st); meow_ui_set_style(st); paint_sb_right(); meow_dirty_full(); } /* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。 * 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走 * font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */ else if (!strcmp(r->key, "font.file")) { mfont_ready = 0; mfont_forced = 1; if (!meow_font_ensure()) logmsg("font.file: reload failed -> dotmatrix fallback"); else logmsg("font.file: reloaded"); meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */ meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */ } /* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换), * 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */ else if (!strcmp(r->key, "font.load_full")) { mfont_ready = 0; mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */ if (!meow_font_ensure()) logmsg("font.load_full: reload failed -> dotmatrix fallback"); else { int lf = 0; meow_settings_get_switch("font.load_full", &lf); logmsg("font.load_full: reloaded (load_full=%d)", lf); } meow_font_reload_multifont(); meow_dirty_full(); } /* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */ else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) { meow_font_reload_multifont(); meow_dirty_full(); } if (save_now) meow_settings_save(); } static void s_set_int_value(const srow_t *r, int v, int save_now) { if (!strcmp(r->key, "audio.volume")) v = vol_clamp(v); /* 尊重 audio.volume_max */ meow_settings_set_int(r->key, v); s_commit(r, save_now); } /* 滑条拖动/点击落值:对「档位型」息屏时间做 索引→秒 换算,其余行原样写盘。 * sl_v 为滑条轨迹值(s_slider_val_from_x 返回值;息屏时间=档位索引)。 */ static void s_slider_set_from_track(const srow_t *r, int sl_v, int save_now) { if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v); /* B2b 深色时段滑条:分钟按 15 分钟步进吸附(0-1439,整 15 分钟),避免拖出任意分钟 */ if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { sl_v = ((sl_v + 7) / 15) * 15; if (sl_v < 0) sl_v = 0; if (sl_v > 1439) sl_v = 1439; } s_set_int_value(r, sl_v, save_now); } /* 滑条拖动值域映射:x(屏幕坐标)→ value,基于布局常量 S_SLIDER_X/W。 * 所有滑条行共用同一 track 几何,故任意滑条行都能正确映射。 */ static int s_slider_val_from_x(const srow_t *r, int px) { int range = r->max - r->min; int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0); if (v < r->min) v = r->min; if (v > r->max) v = r->max; return v; } #if 0 /* M7.2 回退位:旧手绘设置页主列表(手绘分组列表 + s_draw_switch/s_draw_slider + 自算滚动/命中)。 * 新路径走 meow_listview + meow_switch + meow_scroll + meow_select_show;删除 #if 0 行并把 * 下方「M7.2 SDK 路径」的 set_paint_page 调用切回 draw_settings_page 即可一键回退。 */ /* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */ static void draw_settings_page(int sel, int scroll_off) { const meow_ui_theme_t *th = s_ui_theme(); int W = fb_xres, H = fb_yres; fill_rect(0, 0, W, H, th->surface); draw_statusbar(); /* Stage-One:驱动 SDK 全局状态栏(喂数据+标脏顶条) */ if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28); else draw_text(24, 30, "SETTINGS", 3, th->on_surface); fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline); /* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。 * 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底; * 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */ fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant); int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT; s_sel_track_y = -1; for (int i = 0; i < SMENU_N; i++) { int pos = s_vis_of(i); if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */ int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off; if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */ const srow_t *r = &SMENU[i]; int selected = (i == sel); if (selected) meow_ui_fill_rect_alpha(SET_CARD_X + 4, sy, SET_CARD_W - 8, SROW_H, th->primary, 34); meow_ui_fill_rect(SET_CARD_X + 12, sy + SROW_H - 1, SET_CONTENT_W, 1, th->outline); if (r->ctrl == SC_HEADER) { ui_draw(SET_CARD_X + 16, sy + 14, r->label, th->on_surface_variant, 18); } else if (r->ctrl == SC_SWITCH) { ui_draw(SET_CARD_X + 16, sy + (SROW_H - meow_font_line_height(22)) / 2, r->label, th->on_surface, 22); int on = 0; meow_settings_get_switch(r->key, &on); s_draw_switch(SET_CARD_X + SET_CARD_W - 16 - 48, sy + (SROW_H - 28) / 2, on, th); } else if (r->ctrl == SC_SLIDER) { int ly = sy + (SROW_H - meow_font_line_height(22)) / 2 - 6; /* 标题略上移,给下方滑条留位 */ ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); char vb[32]; s_fmt_value(r, vb, sizeof vb); ui_draw(SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22), ly, vb, th->on_surface_variant, 22); int v = 0; meow_settings_get_int(r->key, &v); /* 息屏时间为档位索引型:把秒换算成档位索引再画(各档等距,便于触摸对位); * 其余滑条防御性 clamp 到 [min,max],防止后端暂存值越界(如 brightness 旧值 101 * 在画时被 clamp 到 100,thumb 不冲出 track)。 */ if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v); else { if (v < r->min) v = r->min; if (v > r->max) v = r->max; } int tyy = sy + 40; s_draw_slider(S_SLIDER_X, tyy, S_SLIDER_W, v, r->min, r->max, th); if (selected) s_sel_track_y = tyy; } else if (r->ctrl == SC_INFO) { /* M2.39 只读动态信息(如 WiFi 连接 SSID) */ int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); char vb[48]; s_net_get(r, vb, sizeof vb); int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22); ui_draw(vx, ly, vb, th->on_surface_variant, 22); } else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:摘要 + 右向展开提示 */ int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); char vb[48]; s_net_get(r, vb, sizeof vb); int vx = SET_CARD_X + SET_CARD_W - 16 - 12 - ui_width(vb, 22); /* 给右向 chevron 留位 */ ui_draw(vx, ly, vb, th->on_surface_variant, 22); s_draw_chevron(SET_CARD_X + SET_CARD_W - 16, sy + SROW_H / 2, th->on_surface_variant); } else { /* SC_ENUM / SC_PRESET */ int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); char vb[40]; s_display_value(r, vb, sizeof vb); int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22); ui_draw(vx, ly, vb, th->on_surface_variant, 22); s_draw_chevron(vx - 8, sy + SROW_H / 2, th->on_surface_variant); } } /* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */ int vis_total = s_vis_count() * SROW_H; if (vis_total > SET_CONTENT_H) { int total = vis_total; int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total; if (thumb_h < 24) thumb_h = 24; int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H); meow_gfx_fill_round(fb_xres - 12, SET_CONTENT_Y, 5, SET_CONTENT_H, 3, th->surface_container); meow_gfx_fill_round(fb_xres - 12, th_y, 5, thumb_h, 3, th->on_surface_variant); } /* 底部:返回提示(左)+ 版本号(右),复用应用列表底部对齐逻辑。 * M2.39:返回按钮改用像素左向箭头 + 「返回」——点阵回退字库缺少 '‹' 字形, * 原 「‹ 返回」在 !mfont_ready 时 '‹' 渲染成方框。 */ { char ver[32]; snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); int foot_y = fb_yres - meow_font_line_height(20) - 8; s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant); ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20); ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20); } } #endif /* M7.2 回退位:旧手绘设置页主列表 */ /* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */ static const char *s_preset_value(const srow_t *r, int pick) { int is_cls = r->key && !strncmp(r->key, "font.class_", 11); if (is_cls) { if (pick == 0) return ""; /* 跟随全局 font.file */ pick--; } int pn = 0; const char *const *pps = s_row_presets(r, &pn); if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : ""); return pps[pick]; } /* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)---- * ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。 * 返回选中索引;取消返回 -1。阻塞(自开输入设备)。 * M2.39:modal 核心抽到 s_dd_modal(网络列表复用),本函数只负责组装选项数组。 */ static int s_dropdown(const srow_t *r) { int n = 0, cur = 0; const char *names[8]; char labels[8][48]; const char *item[S_NET_MAX]; if (r->ctrl == SC_ENUM) { int kn = 0; meow_settings_enum_names(r->key, names, &kn); n = kn; if (n > S_NET_MAX) n = S_NET_MAX; for (int i = 0; i < n; i++) { meow_settings_enum_label(r->key, i, labels[i], sizeof labels[i]); item[i] = labels[i]; } int ci = 0; meow_settings_get_enum(r->key, &ci); cur = ci; } else { char cstr[MEOW_SET_MAX_LEN]; meow_settings_get_str(r->key, cstr, sizeof cstr); int pn = 0; const char *const *pps = s_row_presets(r, &pn); int is_cls = r->key && !strncmp(r->key, "font.class_", 11); int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */ int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base; n = base + cnt; if (is_cls) { names[0] = ""; snprintf(labels[0], sizeof labels[0], "跟随全局字体"); item[0] = labels[0]; if (!cstr[0]) cur = 0; } for (int i = 0; i < cnt; i++) { names[base + i] = pps[i]; snprintf(labels[base + i], sizeof labels[base + i], "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]); item[base + i] = labels[base + i]; if (!strcmp(pps[i], cstr)) cur = base + i; } } if (n <= 0) return -1; if (cur < 0 || cur >= n) cur = 0; /* M7.2:枚举/预设选择器迁 SDK meow_select_show(BottomSheet+单选列表,批次2 新控件), * 替换自绘 s_dd_modal(后者仍供 about/网络列表/字体两级子菜单用,勿删)。 */ return meow_select_show(r->label, item, n, cur); } /* ---- 通用选择器下拉模态(MD3 §3.3:居中圆角卡片,列选项)---- * title=标题;item[i]=第 i 项显示文本;n=项数(≤S_NET_MAX);cur=当前值下标(画勾选)。 * 返回选中下标;取消/点卡片外返回 -1。阻塞(自开输入设备)。 * s_dropdown(设置项 enum/preset)与 s_net_open(网络列表)共用。 * M2.40:触摸命中判定改在抬手用抬手时刻最新 abs 坐标(见循环内注释),修「点选项无反应」。 * 说明:模态内部仍保留音量键=移动选项高亮(这是「选值」不是「设置页列表光标」,属另一语义, * 与 M2.39 仅移除 run_settings_page 列表光标移动的约束不冲突)。 */ /* 绘制下拉/关于本机模态:全屏压暗 + 居中圆角卡片 + 标题 + 可见行(按 dd_scroll 裁剪,视口外整行跳过)。 */ static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int d_y, int d_w, int d_h, int title_h, int opt_row, const char *title, const char **item, int n, int sel, int cur, int content_top, int content_bot, int dd_scroll) { meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container); meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline); if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20); else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface); for (int i = 0; i < n; i++) { int ry = d_y + title_h + i * opt_row - dd_scroll; if (ry + opt_row < content_top || ry >= content_bot) continue; /* 视口外整行跳过(配滚动) */ int cur_sel = (i == sel); if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36); ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "", cur_sel ? meow_theme_get()->onPrimary : th->on_surface, 20); if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary); } } int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur) { int sel = cur; const meow_ui_theme_t *th = s_ui_theme(); int W = fb_xres, H = fb_yres; int opt_row = 48, pad = 20, title_h = 40; /* M2.5x 关于本机:内容超高(13 行)时把卡片高钳到屏内,内容区允许滚动(复用设置页列滚动语义)。 */ int raw_d_h = title_h + n * opt_row + pad; int max_card_h = H - SB_BAR_H - 20; /* 卡片可视高上限(不压状态栏、留底边) */ int scrollable = (raw_d_h > max_card_h); int d_h = scrollable ? max_card_h : raw_d_h; int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2; if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */ if (H - d_y - d_h < 20) d_y = H - d_h - 20; /* 内容可视区(卡片体内、标题下、底部内边距上);dd_scroll 仅在 scrollable 时非 0。 */ int content_top = d_y + title_h; int content_bot = d_y + d_h - pad; int dd_view_h = content_bot - content_top; int dd_scroll_max = scrollable ? (n * opt_row - dd_view_h) : 0; if (dd_scroll_max < 0) dd_scroll_max = 0; int dd_scroll = 0; int swipe_inv = 0; if (scrollable) meow_settings_get_switch("input.swipe_invert", &swipe_inv); struct keypoll kp_power, kp_keys; kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); struct kd_state kd_vold, kd_volu, kd_pwr; memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr); int m_prev_vold = 0, m_prev_volu = 0, m_prev_pwr = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; } int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (tfd >= 0) { /* M2.37:彻底清空「打开下拉」这次触摸的残留边沿。goodix 的抬起序列(ABS 坐标/ * BTN_TOUCH=0/MT_TRACKING=-1)在开 fd 后仍可能陆续送达,若只清空一次,第一轮 * 下拉循环会读到这些残边 → 误判抬起/选项错位/点卡片外穿透。故 排空→短延时→再排空。 */ unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} usleep(40000); /* M2.38 Bug3:排空→40ms→再排空,吸收 goodix 抬起序列长尾 */ while (read(tfd, ev, sizeof ev) == 16) {} } int pwr_pending = 0; int result = -1; int dlx = -1, dly = -1, dtd = 0; /* 下拉触摸解析(dlx/dly=最新有效坐标;dtd=已按下) */ /* M2.5x 滚动状态(仅 scrollable 生效;非滚动路径与原文一致) */ int dd_oy = -1; /* 按下原点纵坐标(首个有效 ABS 后锁) */ int dd_scrolling = 0; /* 本次手势是否已判为纵向滚动 */ int dd_scroll_base = 0; /* 进入滚动时锁定的 dd_scroll 基准(连续跟手) */ int sel_moved = 0; /* 音量键改变了 sel(滚动需跟随选中行) */ /* M2.40:不再锁按下时刻的 ddx/ddy —— goodix 时序 BTN_TOUCH 先于 ABS,按下边沿时坐标可能 * 还是 -1/上一手势残留,锁原点会把点击判成「点卡片外=取消」。命中改在抬手时用抬手时刻的 * dlx/dly(抬起时必已更新到本次最新有效坐标),与 meow_touch mt_up 语义同源。 */ /* 初始绘制:全屏压暗覆盖层 + 居中圆角卡片 + 可见行(按 dd_scroll 裁剪) */ dd_modal_paint(th, W, H, d_x, d_y, d_w, d_h, title_h, opt_row, title, item, n, sel, cur, content_top, content_bot, dd_scroll); meow_dirty_full(); meow_ui_flush(); for (;;) { if (sigterm_flag) { result = -1; break; } kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); int vold = 0, volu = 0, pwr = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; } (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_pwr, pwr); vold = kd_vold.stable; volu = kd_volu.stable; pwr = kd_pwr.stable; int need = 0; if (vold && !m_prev_vold && sel < n - 1) { sel++; need = 1; sel_moved = 1; } if (volu && !m_prev_volu && sel > 0) { sel--; need = 1; sel_moved = 1; } if (pwr && !m_prev_pwr) pwr_pending = 1; /* 按下:待抬起确认 */ if (!pwr && m_prev_pwr && pwr_pending) { pwr_pending = 0; result = sel; break; } /* 抬起=确认 */ m_prev_vold = vold; m_prev_volu = volu; m_prev_pwr = pwr; /* 触摸:抬起命中选项=确认,点卡片外=取消 */ if (tfd >= 0) { struct pollfd pfd = { tfd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) { uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); int32_t val = *(int32_t *)(ev + 12); if (type == EV_ABS) { if (code == ABS_X || code == ABS_MT_POSITION_X) dlx = val; else if (code == ABS_Y || code == ABS_MT_POSITION_Y) { dly = val; if (dtd) { if (dd_oy < 0) dd_oy = val; /* 首个有效纵坐标 = 按下原点 */ if (scrollable) { int ddy = val - dd_oy; /* 手指纵向位移(正=下移) */ if (!dd_scrolling && (ddy > S_SCROLL_SLOP || ddy < -S_SCROLL_SLOP)) { dd_scrolling = 1; dd_scroll_base = dd_scroll; /* 触发瞬间锁基准,不跳变 */ } if (dd_scrolling) { /* 标准方向:手指上移(ddy<0)→内容上移/看下方→scroll 增;swipe_invert 反向 */ int t = dd_scroll_base + (swipe_inv ? ddy : -ddy); if (t < 0) t = 0; if (t > dd_scroll_max) t = dd_scroll_max; if (t != dd_scroll) { dd_scroll = t; need = 1; } } } } } } else if (type == EV_KEY && code == BTN_TOUCH) { if (val) { /* 按下边沿:只标「已按下」,绝不锁原点(goodix 时序 BTN_TOUCH 先于 * ABS,此刻 dlx/dly 可能还是 -1 或上一手势残留)。 */ dtd = 1; dd_oy = -1; dd_scrolling = 0; } else if (dtd) { dtd = 0; /* 抬手判定:此刻 dlx/dly 必为本次最新有效坐标(抬起时 ABS 已先到)。 * 卡片内命中 → 返回选项下标;卡片外 → 取消(-1);从未见有效坐标 → 丢弃。 * M2.5x:滚动手势抬手不选(不 launch);命中行换算叠加 dd_scroll。 */ if (dd_scrolling) { dd_scrolling = 0; } else if (dlx >= 0 && dly >= 0) { if (dlx >= d_x && dlx < d_x + d_w && dly >= d_y && dly < d_y + d_h) { int i = (dly - d_y - title_h + dd_scroll) / opt_row; if (i >= 0 && i < n) { sel = i; result = sel; goto ddone; } /* 卡片内但落在标题/内边距:保持弹出态,等待下一次点选 */ } else { result = -1; goto ddone; /* 点卡片外=取消 */ } } /* dlx/dly 为 -1:ghost/残留抬手,丢弃不动作 */ } } } } } if (need) { /* 重绘覆盖层+选项(仅 sel 高亮/滚动变化);sel 经音量键移动时滚动跟随选中行。 */ if (sel_moved && scrollable) { int ry0 = content_top + sel * opt_row - dd_scroll; if (ry0 < content_top) dd_scroll = sel * opt_row; else if (ry0 + opt_row > content_bot) dd_scroll = sel * opt_row - (dd_view_h - opt_row); if (dd_scroll < 0) dd_scroll = 0; if (dd_scroll > dd_scroll_max) dd_scroll = dd_scroll_max; } sel_moved = 0; dd_modal_paint(th, W, H, d_x, d_y, d_w, d_h, title_h, opt_row, title, item, n, sel, cur, content_top, content_bot, dd_scroll); meow_dirty_full(); meow_ui_flush(); } usleep(POLL_MS * 1000); } ddone: if (tfd >= 0) close(tfd); kp_close(&kp_power); kp_close(&kp_keys); return result; } /* 三级 (family,weight) 下多 tier 的选档偏好分(越大越优先;设置页写 font.file/class 值用)。 * 关(font.load_full=0)→ common 优先(回退 gbk→full);开 → gbk 优先(回退 full→common)。 */ static int font_tier_pick_rank(const char *tier, int load_full) { int is_common = tier && !strcmp(tier, "common"); int is_gbk = tier && !strcmp(tier, "gbk"); int is_full = tier && !strcmp(tier, "full"); if (!load_full) { if (is_common) return 3; if (is_gbk) return 2; if (is_full) return 1; return 0; } if (is_gbk) return 3; if (is_full) return 2; if (is_common) return 1; return 0; } /* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构;M2.8 三级适配)---- * 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入 * 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径 * MeowOS/fonts///font.png(两级)或 ...////font.png(三级, * 或 class 行「跟随全局」=空串)。 * M2.8 三级冲突:同 (family,weight) 出现多个 tier → 选择器仍为 family→weight 两级、不新增 tier * 级菜单;每 weight 只显一行(代表 = 按「当前加载大字库」开关选档:关=common 优先、开=gbk 优先, * 见 font_tier_pick_rank),写值 = 该代表 tier 的卡根相对路径。 * 复用 s_dd_modal(与 WiFi/关于 同款模态)。返回 1=已选中(out_val 已填),0=取消。 */ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen) { if (!r) return 0; int is_cls = r->key && !strncmp(r->key, "font.class_", 11); char cur[MEOW_SET_MAX_LEN]; meow_settings_get_str(r->key, cur, sizeof cur); char curfam[64] = "", curwt[32] = ""; if (cur[0]) font_family_weight_from_png(cur, curfam, sizeof curfam, curwt, sizeof curwt); /* ---- 一级:去重 family 列表(class 行首项「跟随全局」)---- */ char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64]; int famn = 0; if (is_cls) { if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], ""); snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; } } for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) { const char *f = font_preset_fams[i] ? font_preset_fams[i] : ""; int dup = 0; for (int k = 0; k < famn; k++) if (!strcmp(fam_slug[k], f)) { dup = 1; break; } if (dup) continue; snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", f); snprintf(fam_disp[famn], sizeof fam_disp[0], "%s", (font_preset_names[i] && font_preset_names[i][0]) ? font_preset_names[i] : f); famn++; } if (famn <= 0) return 0; const char *fitem[S_NET_MAX]; char ftext[S_NET_MAX][64]; for (int i = 0; i < famn; i++) { snprintf(ftext[i], sizeof ftext[i], "%s", fam_disp[i]); fitem[i] = ftext[i]; } int cur_fam = is_cls ? 0 : -1; for (int i = is_cls ? 1 : 0; i < famn; i++) if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i; int pick = s_dd_modal(r->label, fitem, famn, cur_fam); if (pick < 0) return 0; if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */ const char *fam = fam_slug[pick]; /* ---- 二级:该 family 的字重列表(去重:同 (family,weight) 只显一行)---- * 三级结构下同 (family,weight) 出现多个 tier(common/gbk/full)→ 不新增 tier 级菜单,按 * 「当前加载大字库」开关定档:关=common 优先、开=gbk 优先(font_tier_pick_rank)。 */ int load_full = 0; meow_settings_get_switch("font.load_full", &load_full); char wt_pick[S_NET_MAX][32]; /* 该行代表的 weight slug */ int wt_rank[S_NET_MAX]; /* 该行当前档位偏好分(保留最高分 tier 为行代表) */ char wt_val[S_NET_MAX][MEOW_SET_MAX_LEN]; char wt_text[S_NET_MAX][96]; int wn = 0; for (int i = 0; i < font_presets_n; i++) { if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue; const char *wt = font_preset_wts[i] ? font_preset_wts[i] : "regular"; const char *tier = font_preset_tiers[i] ? font_preset_tiers[i] : "common"; int rank = font_tier_pick_rank(tier, load_full); int slot = -1; for (int k = 0; k < wn; k++) if (!strcmp(wt_pick[k], wt)) { slot = k; break; } if (slot < 0) { if (wn >= S_NET_MAX) break; snprintf(wt_pick[wn], sizeof wt_pick[0], "%s", wt); wt_rank[wn] = -1; slot = wn; wn++; } if (rank > wt_rank[slot]) { /* 保留档位偏好最高的一套作为该 weight 行代表 */ wt_rank[slot] = rank; snprintf(wt_val[slot], sizeof wt_val[0], "%s", font_presets[i]); snprintf(wt_text[slot], sizeof wt_text[0], "%s", font_preset_labels[i]); } } if (wn <= 0) return 0; /* 该 family 无字重(理论不出现:能进 family 必已探到字重) */ const char *witem[S_NET_MAX]; for (int i = 0; i < wn; i++) witem[i] = wt_text[i]; /* 当前位置高亮:按当前 font.file 的 (family,weight) 命中所显示 weight 行(不依赖 tier 精确匹配, * 因三级下 font.file 具体 tier 可能与该行代表 tier 不同)。 */ int cur_wt = -1; { char cfam2[64] = "", cwt2[32] = ""; if (cur[0]) font_family_weight_from_png(cur, cfam2, sizeof cfam2, cwt2, sizeof cwt2); for (int i = 0; i < wn; i++) if (cwt2[0] && !strcmp(wt_pick[i], cwt2)) { cur_wt = i; break; } } int wp = s_dd_modal(r->label, witem, wn, cur_wt); if (wp < 0) return 0; snprintf(out_val, vlen, "%s", wt_val[wp]); return 1; } /* ================================================================== * M7.2 设置页渲染层迁移(主列表迁 SDK 控件): * - 分组列表 → meow_listview(虚拟化 set_vsource 固定 item 池 + section 段头 + set_item_draw 自绘); * - 开关行 → 行内真 meow_switch(池化,SDK 绘制 + 按压态); * - 滑条行 → 行内嵌 meow_slider(自绘 track/thumb,保留既有 s_draw_slider 语义色; * 「行内嵌自绘」,M7 定案允许)——拖动实时写 settings.conf(读改写单键, * 经 meow_settings_set_*,勿整文件覆盖——音量存盘教训); * - 枚举/预设 → SDK meow_select_show(BottomSheet+单选列表,替换自绘 s_dd_modal); * - 信息/列表行 → 行 right 值 + chevron(SDK 主题语义色)。 * 滚动 → meow_scroll(跟手 track + 松手 fling,同 subpage.c M7.1a 语义;刻意不 set_snap)。 * 控件 lazy 创建一次(owner=SET_SENTINEL_ACT 哨兵),跨多次进入复用;数据/动作(s_commit、 * s_set_int_value、s_net_*、s_font_dropdown、s_dropdown)逻辑层不动,仅换渲染层 + 命中; * run_settings_page 会话骨架(触摸/滚动/返回/IPC/电源键语义)不改。旧手绘 draw_settings_page * + s_clamp_scroll + s_row_at_content_y 保留 #if 0 作回退位(M7 红线:一键回退)。 * ================================================================== */ #define SET_SENTINEL_ACT 0x53 /* 设置页哨兵 activity(不与 launcher 其它页冲突) */ #define SET_LIST_Y SET_CONTENT_Y /* 100:列表顶缘(顶栏/标题下) */ #define SET_FOOT_H 40 /* 底部返回/版本带高 */ #define SET_ITEM_H 68 /* MD3 行高(滑条行给 track 留位) */ #define SET_SLIDER_YOFF 45 /* 滑条行内 track 顶缘相对行顶(÷s_draw_slider h=6,中心=顶+3) */ #define SET_TRACK_CY 48 /* 滑条 track 中心相对行顶 y(=SET_SLIDER_YOFF+3,与绘制对齐) */ #define SET_SLIDER_LY 12 /* 滑条行 label 顶相对行顶 y(上移给 track 留位) */ #define SET_SWITCH_POOL 12 static meow_widget *g_set_lv = NULL; /* meow_listview(虚拟化) */ static meow_scroll *g_set_sc = NULL; /* 滚动物理 */ static meow_widget *g_set_sw[SET_SWITCH_POOL]; /* 行内开关池(真 meow_switch) */ static int g_set_sw_n = 0, g_set_sw_use = 0; /* 池创建数 / 本轮已用(每次整绘复位) */ static int g_set_lv_h = 0; /* 列表视口高(选中可见性 clamp 用) */ static int g_set_init = 0; static void set_lv_rect(int *x, int *y, int *w, int *h) { if (x) *x = 0; if (y) *y = SET_LIST_Y; if (w) *w = fb_xres; if (h) { int t = fb_yres - SET_LIST_Y - SET_FOOT_H; if (t < 0) t = 0; *h = t; } } static int set_lv_count(void *ctx) { (void)ctx; return s_vis_count(); } /* 行填充(虚拟化 pool bind;段头行 section=1、enabled=1 供 SDK 命中/段头绘制) */ static void set_lv_bind(int idx, meow_lv_item *out, void *ctx) { (void)ctx; memset(out, 0, sizeof *out); out->icon_kind = MEOW_LIST_ICON_NONE; out->enabled = 1; if (idx < 0 || idx >= s_vis_count()) { out->title = ""; return; } const srow_t *r = &SMENU[s_vis_at(idx)]; out->title = r->label ? r->label : ""; out->section = (r->ctrl == SC_HEADER) ? 1 : 0; if (r->ctrl == SC_SWITCH) { int on = 0; meow_settings_get_switch(r->key, &on); out->right = MEOW_LIST_RIGHT_SWITCH; out->on = on; } } /* 行内开关池:取下一个可用槽(每帧 set_lv_draw 顺序取用,整绘前复位 g_set_sw_use) */ static meow_widget *set_sw_take(void) { if (g_set_sw_use < g_set_sw_n) return g_set_sw[g_set_sw_use++]; if (g_set_sw_n >= SET_SWITCH_POOL) return NULL; meow_widget *w = meow_switch_create(0, 0, 0); if (w) { g_set_sw[g_set_sw_n++] = w; return g_set_sw[g_set_sw_use++]; } return NULL; } /* 整行自绘(set_item_draw 覆盖默认 renderer;x=列表左缘、y=行顶、w/h=行全宽高, * state=SELECTED/PRESSED/DISABLED 组合;颜色全取 meow_theme 语义色,无散落硬编码) */ static void set_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx) { (void)ctx; (void)h; const meow_theme *mt = meow_theme_get(); if (idx < 0) return; int si = s_vis_at(idx); const srow_t *r = &SMENU[si]; int sel = (state & MEOW_LISTVIEW_ST_SELECTED); int press = (state & MEOW_LISTVIEW_ST_PRESSED); int dis = (state & MEOW_LISTVIEW_ST_DISABLED); int thh = meow_ui_text_height(mt->font_body); uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface; uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant; if (r->ctrl == SC_HEADER) { meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh); int ty = y + (SET_ITEM_H - thh) / 2; if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary); meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant); return; } if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer); else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26); int ly = y + (SET_ITEM_H - thh) / 2; /* label 单行垂直居中 */ switch (r->ctrl) { case SC_SWITCH: { meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); meow_widget *sw = set_sw_take(); if (sw) { int on = 0; meow_settings_get_switch(r->key, &on); meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32); meow_switch_set(sw, on); meow_widget_draw(sw); } break; } case SC_SLIDER: { /* label 上移一行,track 放行下方(中心=SET_SLIDER_YOFF+3=48),与拖动命中带对齐 */ meow_ui_text(x + 16, y + SET_SLIDER_LY, r->label, mt->font_body, textcol); char vb[32]; s_fmt_value(r, vb, sizeof vb); meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), y + SET_SLIDER_LY, vb, mt->font_body, subcol); int v = 0; meow_settings_get_int(r->key, &v); /* 息屏时间为档位索引型(秒→索引 等距画);其余 clamp 到 [min,max] */ if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v); else { if (v < r->min) v = r->min; if (v > r->max) v = r->max; } s_draw_slider(S_SLIDER_X, y + SET_SLIDER_YOFF, S_SLIDER_W, v, r->min, r->max, s_ui_theme()); break; } case SC_INFO: { meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); char vb[48]; s_net_get(r, vb, sizeof vb); meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol); break; } case SC_LIST: { meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); char vb[48]; s_net_get(r, vb, sizeof vb); int vx = x + w - 16 - 12 - meow_ui_text_width(vb, mt->font_body); meow_ui_text(vx, ly, vb, mt->font_body, subcol); s_draw_chevron(x + w - 16, y + SET_ITEM_H / 2, subcol); break; } default: { /* SC_ENUM / SC_PRESET */ meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); char vb[40]; s_display_value(r, vb, sizeof vb); int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body); meow_ui_text(vx, ly, vb, mt->font_body, subcol); s_draw_chevron(vx - 8, y + SET_ITEM_H / 2, subcol); break; } } meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant); if (dis) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90); } /* ---- meow_scroll 回调(包 listview)---- */ static void set_sc_set(void *ctx, int off) { (void)ctx; if (g_set_lv) meow_listview_set_scroll(g_set_lv, off); } static int set_sc_max(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_max_scroll(g_set_lv) : 0; } static int set_sc_get(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_scroll(g_set_lv) : 0; } /* 列表区重绘(含内容底 + listview),供 fling/滚动局部刷。 */ static void set_redraw_region(void *ctx) { (void)ctx; int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh); if (lvh <= 0) return; const meow_ui_theme_t *th = s_ui_theme(); meow_ui_fill_rect(lvx, lvy, lvw, lvh, th->surface_variant); if (g_set_lv) { g_set_sw_use = 0; meow_widget_draw(g_set_lv); } meow_dirty_add(lvx, lvy, lvw, lvh); meow_ui_flush(); } /* 选中行可见性:滚动到选中行不越界(仅靠 tap 触发,选中行即在视野内,防御性兜底)。 */ static void set_ensure_sel_visible(int sel) { if (!g_set_lv) return; int p = s_vis_of(sel); if (p < 0) return; int scroll = meow_listview_scroll(g_set_lv); int top = p * SET_ITEM_H, bot = top + SET_ITEM_H; if (top < scroll) meow_listview_set_scroll(g_set_lv, top); else if (bot > scroll + g_set_lv_h) meow_listview_set_scroll(g_set_lv, bot - g_set_lv_h); } /* 创建设置页控件集(owner=SET_SENTINEL_ACT;仅首进创建一次,跨次进入复用)。 */ static void set_widgets_create(void) { meow_ui_activity_push(SET_SENTINEL_ACT); int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh); g_set_lv_h = lvh; g_set_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); if (g_set_lv) { meow_listview_set_vsource(g_set_lv, set_lv_count, set_lv_bind, NULL); meow_listview_set_item_draw(g_set_lv, set_lv_draw, NULL); meow_listview_set_item_h(g_set_lv, SET_ITEM_H); /* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标会往回拉 */ } g_set_sc = meow_scroll_create(set_sc_set, set_sc_max, set_sc_get, set_redraw_region, NULL); meow_ui_activity_pop(); g_set_init = 1; } /* 整页绘制(进 back_buf;调用方 meow_dirty_full/meow_dirty_add + flush)。sel=当前选中(SMENU 下标)。 */ static void set_paint_page(int sel) { const meow_ui_theme_t *th = s_ui_theme(); int W = fb_xres, H = fb_yres; fill_rect(0, 0, W, H, th->surface); draw_statusbar(); if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28); else draw_text(24, 30, "SETTINGS", 3, th->on_surface); fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline); fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant); if (g_set_lv) { g_set_sw_use = 0; int p = s_vis_of(sel); if (p >= 0) meow_listview_set_sel(g_set_lv, p); meow_widget_draw(g_set_lv); } /* 底部:返回(左)+ 版本(右),复用应用列表底部对齐逻辑 */ char ver[32]; snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); int foot_y = fb_yres - meow_font_line_height(20) - 8; s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant); ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20); ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20); } /* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */ void run_settings_page(void) { int sel = s_first_setting(); int H = fb_yres; unsigned long long t_sets = now_ms(); /* M3 取证:进设置页耗时分段(真机核对用) */ /* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts///,刷新「字体文件」下拉。 * 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 * M3:verify=0 —— 跳过逐套 sha256 校验(gbk 套 6.8MB 级,逐套双 sha256 真机耗数秒), * 只枚举+读 font.json+落 index,入口不再被 sha256sum 拖住;完整性校验留给启动全量扫描。 */ meow_font_scan_presets(0); logmsg("settings entry: font rescan %llums", now_ms() - t_sets); struct keypoll kp_power, kp_keys; kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; } int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } unsigned long long touch_cooldown_until = now_ms() + 300; unsigned long long last_tick = now_ms(); unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */ int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */ unsigned long long net_refresh_pump_at = 0; /* M3:异步网络刷新下一推进时刻(0=立即) */ /* 触摸拖动滑条状态 */ int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; int drag = -1; /* 被拖动的行索引;-1=无 */ /* M2.37 列表拖动滚动状态 */ int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */ int drag_last_y = 0; /* M7.2:滚动跟手增量基准(meow_scroll_track 速度采样) */ int scroll_y0 = 0; /* 按下原点纵向坐标 */ int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */ int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */ int wake_suppress = 0; /* 自动息屏唤醒后:吸收唤醒键边沿,待全部抬起再接管 */ /* M2.x(KEY_CONTROL_DESIGN §2)电源键状态:设置页电源键 = 系统级语义(非返回), * 对照主界面 run_menu 3130-3137 的同名变量。单击=息屏/亮屏、长按=电源菜单、8s=强关。 */ unsigned long long pwr_hold_at = 0; /* 本次电源按下计时起点 */ int pwr_menu_state = 0; /* 是否已因此次 hold 弹过电源菜单 */ int pwr_woke = 0; /* 本次按下=自动息屏唤醒(释放只唤醒不 toggle) */ int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */ /* 首次全帧绘制 */ /* M2.39/M3:开关行同步真实硬件态(快)+ 异步 kick 网络刷新 —— 不再在入口同步跑 * wpa_cli/bluetoothctl 串行 popen(真机 ≥5s 卡顿根因);状态/列表由下方主循环逐步骤 * s_net_refresh_pump() 填充,draw 用缓存(g_net_*),数据变化即 need_paint。 */ s_net_sync_from_sys(); logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets); if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */ meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */ set_paint_page(sel); meow_dirty_full(); meow_ui_flush(); logmsg("settings entry: page shown in %llums", now_ms() - t_sets); for (;;) { unsigned long long now = now_ms(); if (sigterm_flag) break; /* 重绘触发:值/选中/滚动变化 */ int need_paint = 0; /* ---- M2.37 自动息屏态:留在设置页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- * 对照 run_menu 主循环 meow_auto_slept 的唤醒重绘(M2.17/2.18)。休眠期间仅探测 * 输入(按键原始按下 / black 模式触摸按下);一旦唤醒:亮屏、清自动态并整帧重绘。 */ if (meow_auto_slept && !meow_screen_on) { kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); int wake = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1; } if (!wake && screen_off_mode == 1 && tfd >= 0) { /* black 模式:面板仍上电,触摸可唤醒 */ struct pollfd spfd = { tfd, POLLIN, 0 }; if (poll(&spfd, 1, 0) > 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) { uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); int32_t val = *(int32_t *)(ev + 12); if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1; } } } if (wake) { meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms(); /* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); touch_cooldown_until = now_ms() + 300; td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1; logmsg("settings: auto screen-off -> input wake, stayed in settings page"); set_paint_page(sel); meow_dirty_full(); meow_ui_flush(); } usleep(POLL_MS * 1000); continue; } /* ---- M3:异步网络刷新(每 ~150ms 推进一步,每步一个有界 popen)---- * 入口已同步 draw(用 g_net_* 缓存),此处逐步填充 SSID/已保存/附近/蓝牙角色, * 数据变化则 need_paint=1 让行值即时刷新,绝不在某一同步调用里连续跑数条 popen。 */ if (now >= net_refresh_pump_at) { net_refresh_pump_at = now + 150; if (s_net_refresh_pump()) need_paint = 1; } /* ---- 按键 ---- */ kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); int vold = 0, volu = 0, play = 0, pwr = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; } (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); vold = kd_vold.stable; volu = kd_volu.stable; play = kd_play.stable; pwr = kd_pwr.stable; if (wake_suppress) { /* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作, * 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */ if (!pwr && !vold && !volu && !play) wake_suppress = 0; } else { /* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束: * 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。 * 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */ if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } /* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」: * 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。 * 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */ if (play && !m_prev_play) { logmsg("settings: play key caught (media-reserved, no exit)"); last_act = now; } /* M2.x(KEY_CONTROL_DESIGN §2)电源键 = 系统级语义,对齐主界面 run_menu 3284-3366: * 单击(按<长按阈值后松开)= 息屏/亮屏;长按>=longpress_menu_ms = 电源菜单; * 按住>=forceoff_ms(且未开过菜单)= 强制关机;自动息屏按下只唤醒。 * ※ 电源键在设置页不再映射为「返回」——退出只能靠触摸(底部返回按钮 + 左缘左滑)。 */ if (pwr && !m_prev_pwr) { pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ last_act = now; /* 视为输入,防按住期间误触发自动息屏 */ /* M2.18 Bug 2 同款:按下正落在自动息屏态(auto_slept 且屏已关)→ 本次只做唤醒 */ if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; } if (pwr && pwr_hold_at) { /* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state) * 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */ if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) { if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); } /* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */ } else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) { pwr_menu_state = 1; logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); show_power_menu(); /* 动作在函数内 system();取消则续循环 */ last_act = now_ms(); /* 菜单返回即视为「刚有输入」,重置自动息屏计时 */ /* M2.11 B 同款:菜单已关闭。清空按住计时与菜单态,绝不把已互动的 hold 再累计到 8s 强关。 */ pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */ /* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配, * 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */ need_paint = 1; /* 已按 raw 对齐 m_prev_*,跳过下方基线复写,防把 raw 值覆盖回旧稳定值 → 假单边沿 */ goto settings_keys_seeded; } } if (!pwr && m_prev_pwr) { /* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */ if (pwr_suppress_click) { logmsg("settings pm: power release after power-menu -> no toggle (suppress)"); } else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); meow_toggle_panel(); meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ last_act = now_ms(); need_paint = 1; /* 亮屏重绘 / 息屏留屏(面板已黑,下次亮屏重绘) */ } else if (pwr_woke) { logmsg("settings pm: power release after auto screen-off wake -> no toggle (wake only)"); } pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ } } /* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */ if (vol_ramp_dir != 0) { int held = (vol_ramp_dir > 0) ? volu : vold; int going = vol_ramp_tick(held); g_sys.vol = settings_volume_get(); paint_sb_right(); need_paint = 1; if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol); } m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; settings_keys_seeded: ; /* ---- 触摸 ---- */ if (tfd >= 0 && meow_screen_on) { /* M2.x:手动电源息屏后不处理触摸(与主界面一致,且防屏黑时误退出/误改值) */ struct pollfd pfd = { tfd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) { uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); int32_t val = *(int32_t *)(ev + 12); if (type == EV_ABS) { if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val; else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val; if (td && !td_ok && tlx >= 0 && tly >= 0) { tdx = tlx; tdy = tly; td_ok = 1; meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(行按压态渲染) */ scroll_y0 = tdy; /* 列表拖动基准:按下原点 */ /* 按下原点落在滑条行 track 带内 → 开始拖动(meow_listview hit 判行) */ int vp = g_set_lv ? meow_listview_hit(g_set_lv, tlx, tly) : -1; if (vp >= 0 && drag < 0) { const srow_t *rr = &SMENU[s_vis_at(vp)]; if (rr->ctrl == SC_SLIDER) { int track_cy = SET_LIST_Y + vp * SET_ITEM_H - meow_listview_scroll(g_set_lv) + SET_TRACK_CY; if (tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 && tly >= track_cy - 14 && tly <= track_cy + 14) { drag = s_vis_at(vp); int v = s_slider_val_from_x(rr, tlx); s_slider_set_from_track(rr, v, 0); need_paint = 1; } } } } /* M7.2 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。 * 走 meow_scroll(跟手 track + 松手 fling),同 subpage.c M7.1a 语义; * 方向经 input.swipe_invert 切换(默认手指上滑 → 内容下翻)。 */ if (td_ok && drag < 0 && !scroll_drag) { int ddy = tly - scroll_y0; if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { scroll_drag = 1; meow_settings_get_bool("input.swipe_invert", &scroll_invert); meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */ drag_last_y = tly; } } if (scroll_drag && td_ok) { int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */ drag_last_y = tly; int scdy = scroll_invert ? dydt : -dydt; if (g_set_sc && scdy) meow_scroll_track(g_set_sc, scdy, now); /* 列表滚动不在此即时重绘:整批事件末尾由下方「if (scroll_drag && td_ok) * set_redraw_region」合并为一次视口局部重绘。此处若 set_redraw_region 且再设 * need_paint=1,底部 set_paint_page 会再走一遍整页重绘 + dirty_add(0,42,...) * 近整屏 flush —— 快滑每帧整页重绘 → 卡顿,违背「局部脏区不整屏重绘」铁律; * 且一个 poll 多个移动事件各自 flush 会多次 FBIO_WAITFORVSYNC 阻塞放大延迟。 */ } if (drag >= 0 && td_ok) { const srow_t *r = &SMENU[drag]; int v = s_slider_val_from_x(r, tlx); s_slider_set_from_track(r, v, 0); /* 拖动实时 set_*,抬起才落盘 */ need_paint = 1; } } else if (type == EV_KEY && code == BTN_TOUCH) { if (val && !td) { if (now_ms() < touch_cooldown_until) { tlx = tly = -1; /* 冷却丢弃 */ } else { td = 1; td_ok = 0; tlx = -1; tly = -1; drag = -1; scroll_drag = 0; } } else if (!val && td) { td = 0; meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */ if (drag >= 0) { /* 滑条拖动结束 → 落盘 */ const srow_t *r = &SMENU[drag]; s_commit(r, 1); drag = -1; need_paint = 1; } else if (scroll_drag) { scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */ if (g_set_sc) meow_scroll_fling(g_set_sc); /* 松手 fling(惯性 ease-out) */ scroll_y0 = 0; need_paint = 1; } else if (td_ok) { int dx = tlx - tdx, dy = tly - tdy; /* M2.39 左缘右滑返回:复用全局左缘返回判定 meow_edge_back() * (与 SDK 全局左缘返回 A-0 同语义:左缘起点 + 右滑≥MEOW_EDGE_SWIPE * 且横向主导),避免设置页自造一套重复判定。 */ if (meow_edge_back(tdx, dx, dy)) { if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } goto done; } int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; if (adx < 60 && ady < 60) { /* 视为 tap */ int tx = tdx, ty = tdy; /* 底部返回按钮 */ if (ty >= H - 52 && tx >= SET_CARD_X && tx <= SET_CARD_X + 120) { if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } goto done; } /* M7.2:行命中走 meow_listview(含滚动换算 + 禁用/越界行不响应); * 段头行 enabled=1 故 hit 亦命中 → 下方按 ctrl==SC_HEADER 分流。 */ int vp = g_set_lv ? meow_listview_hit(g_set_lv, tx, ty) : -1; int idx = (vp >= 0) ? s_vis_at(vp) : -1; if (idx >= 0 && !s_setting_idx(idx)) { /* 点分组标题行 → 仅选中下一设置行 */ int ns = s_next_setting(idx); sel = ns; need_paint = 1; sel_changed = 1; } else if (idx >= 0) { const srow_t *r = &SMENU[idx]; sel = idx; need_paint = 1; sel_changed = 1; if (r->ctrl == SC_SWITCH) { int on = 0; meow_settings_get_switch(r->key, &on); meow_settings_set_switch(r->key, !on); s_commit(r, 1); } else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd * (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 * M2.76:WiFi/蓝牙升级为独立子页面 → 点开进 run_wifi_page/ * run_bt_page(阻塞,内部自开 fds;返回后仍须排空本页 tfd)。 */ after_modal = 1; if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh_kick(); } else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh_kick(); } else if (r->net_list == NET_LIST_PIN_PAGE) { run_lock_settings_page(); s_net_refresh_kick(); } else s_net_open(r); if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} usleep(40000); while (read(tfd, e, sizeof e) == 16) {} } td = 0; td_ok = 0; drag = -1; scroll_drag = 0; scroll_y0 = 0; tdx = tdy = tlx = tly = -1; touch_cooldown_until = now_ms() + 300; memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); need_paint = 1; } else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) { /* 单次点击即开选择器子对话框 */ after_modal = 1; /* 关闭后整帧重绘(还原被 dim 的状态栏) */ if (s_row_is_font(r)) { /* M2.6 字重:字体行走「family→字重」两级子菜单 */ char fval[MEOW_SET_MAX_LEN]; if (s_font_dropdown(r, fval, sizeof fval)) { meow_settings_set_str(r->key, fval); s_commit(r, 1); } } else { int pick = s_dropdown(r); if (pick >= 0) { if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick); else meow_settings_set_str(r->key, s_preset_value(r, pick)); s_commit(r, 1); } } /* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd + * 重置触摸基线。根因:input 子系统把事件广播到所有 * 打开的 fd —— s_dropdown 用独立 fd 消费选项 tap,但 * 本页 tfd 同样收到了整套下拉会话事件;若不清空, * 外层 while(read) 会把它们当新 tap → 穿透到下层设置项。 * 故 排空→40ms→再排空 + 重置触摸状态 + 冷却窗口吸收长尾。 */ if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} usleep(40000); while (read(tfd, e, sizeof e) == 16) {} } td = 0; td_ok = 0; drag = -1; scroll_drag = 0; scroll_y0 = 0; tdx = tdy = tlx = tly = -1; touch_cooldown_until = now_ms() + 300; /* 重置设置页按键防抖基线,防残边 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); need_paint = 1; } else if (r->ctrl == SC_SLIDER && tlx >= 0) { int v = s_slider_val_from_x(r, tlx); s_slider_set_from_track(r, v, 1); } } } } td_ok = 0; } } } } /* 列表拖动滚动:本批连续触摸事件合并为一次视口局部重绘(只刷列表区, * 最多一次 present),不再触发 need_paint 整页重绘。scroll_drag/td_ok 在 * 抬手处置 0(抬手那轮已走 meow_scroll_fling + need_paint 整页),故此处只兜活动滚动。 */ if (scroll_drag && td_ok) set_redraw_region(NULL); } /* ---- 滚动 + 重绘 ---- */ if (need_paint) { last_act = now; /* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */ if (sel_changed) { set_ensure_sel_visible(sel); sel_changed = 0; } set_paint_page(sel); /* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化), * 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。 * 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */ if (after_modal) { meow_dirty_full(); after_modal = 0; } else meow_dirty_add(0, 42, fb_xres, fb_yres - 42); meow_ui_flush(); } /* M2.37 超时自动息屏(与主界面一致):空闲 > screen_off_timeout → 关屏,但留在设置页: * 置 meow_auto_slept 进入休眠态,由顶部睡眠门等待任意输入唤醒并重绘设置页、不丢上下文 * (对照 run_menu 外层 meow_auto_slept 的唤醒重绘,M2.17/2.18 同款语义)。 */ if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && (unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) { meow_panel_off(); meow_auto_slept = 1; wake_suppress = 1; /* 唤醒输入边沿待完全抬起后再接管,避免误动作 */ logmsg("settings: idle %ds -> auto screen-off (stay in settings)", screen_off_timeout); } /* ~1s 状态栏时钟刷新 */ if (now - last_tick >= 1000) { last_tick = now; draw_clock(); fb_norm_buf0(); meow_ui_flush(); } /* M7.2: 推进 fling/回弹动画(meow_scroll 惯性停;无动画零开销) */ meow_anim_tick(); usleep(POLL_MS * 1000); } done: meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */ meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */ if (tfd >= 0) close(tfd); kp_close(&kp_power); kp_close(&kp_keys); logmsg("settings page closed"); }