feat(hello): 控件库demo演示页(agent-159) — M2.71 Material组件展示+交互

- home加「WIDGETS DEMO」入口 + ACT_DEMO activity; demo控件启动push一次创建(owner=ACT_DEMO, 命中只owner==active, home/demo零重建)
- 覆盖: Button五型+禁用/Switch/Slider/TextField(阻塞IME)/Checkbox/Radio(互斥)/ProgressBar(线圆动画)/Card(嵌子按钮)/Divider/Badge(3)/TabBar/NavigationBar/TopAppBar(返回箭头)/Dialog+BottomSheet/FAB/LazyColumn(100项)
- 交互真实控件状态机; 配色全取meow_theme(随MD3/MIUIx+明暗); 局部刷d_redraw(矩形)+meow_ui_flush_region不整屏不冲状态栏; 原home逻辑逐字未改
- 交叉编译0warning; hello=803b23fb meow_ui.a=92e61fb8(未变未碰SDK); 宿主离屏PPM目检3帧渲染正确
- 遗留真机观感; 滑动=按scroll重建内容控件(SDK无位置setter, 方案§0记录修正)
This commit is contained in:
MeowOS committed 2026-08-30 23:28:50 +08:00
1 parent c807934561
commit 4ff0d99398
2 files changed
+462 -118

No files matched your search

BIN
View File
Binary file not shown.
+462 -118
View File
@@ -2,17 +2,39 @@
*
* 契约(MEOW_APP_SDK.md §1/§3/§6):应用只调 meow_ui API —— 屏幕(back_buf +
* flush 上屏)与输入(meow_ui_run 事件循环)由框架接管;不再 open/mmap/ioctl
* /dev/fb0、不再读 /dev/input/event*。本应用验证「清屏 + 画文本 + 画按钮 +
* 响应触摸」这一最小工作闭环,并集成了通知触发按钮(meow_notify_send,§9.1)
* 用于端到端测试通知链路(SDK → IPC → launcher 中央存储 → 10s TTL 落盘)。
* /dev/fb0、不再读 /dev/input/event*。
*
* 两大职责:
* A) 原有:验证「清屏 + 画文本 + 画按钮 + 响应触摸」最小闭环 + 通知触发按钮
* (meow_notify_send,§9.1)端到端测试通知链路;
* B) 新增:**控件库 Demo 演示页**(§5.8 第一/二批组件)——作为 SDK 控件库的
* 验证载体 + 活文档:用真实控件(meow_btn_* / meow_switch_* / meow_slider_* /
* meow_widget_custom_create / meow_*)搭「展示 + 可交互」界面,用户刷 hello
* 从 home 的「WIDGETS DEMO」按钮进入,逐项验证各组件外观 / 命中 / 状态。
*
* 交互:
* - 点按钮 → tap 计数 +1(按钮标签实时更新);
* - 通知触发行(6 个按钮):每个点击 meow_notify_send() 发一条不同参数的通知,
* 覆盖 普通INFO / 媒体(单例) / HIGH紧急度 / 进度(40%) / 常驻 / 跳转Activity;
* - CANCEL LAST 按钮:meow_notify_cancel(最近一次 send 的 id);
* - 左缘左滑(抬手触发)→ 请求退出(回应用列表);滑动过程顶部出现随位移增长的进度条;
* - 空闲时 on_tick 每秒刷新一行状态(含运行秒数)。
* - home:点 TAP ME → 回圈计数;6 个通知触发行;CANCEL LAST;FULLSCREEN;
* 新增「WIDGETS DEMO」按钮 → 进入 demo 页;左缘左滑 → 退出。
* - demo 页:TopAppBar 返回箭头 / 左缘左滑 → 回 home;内容区展示并交互
* Button 五型(含禁用) / Switch / Slider / TextField(点开阻塞 IME) / Checkbox /
* Radio(单选组) / ProgressBar(线性+圆形带动画) / Card(嵌子项) / Divider /
* Badge / TabBar / NavigationBar(底部) / TopAppBar(顶部) / Dialog+BottomSheet /
* FAB / LazyColumn(100 项列表,swipe 滚动)。配色取 meow_theme 语义色板(随系统
* MD3/MIUIx + 明暗自动切,零硬编码品牌色)。交互局部刷新(meow_ui_flush_region,
* 不整屏、不冲状态栏)。
*
* 页面/Activity(§6.1 ①):home(根,ACT_HOME 0)、全屏(ACT_FULL 1)、demo(ACT_DEMO 2)
* 三态互斥;控件 owner=创建时 active Activity,命中只在 owner==active 生效(根治跨页
* 残留)。demo 控件在启动 push(ACT_DEMO) 下一次性创建(owner=ACT_DEMO),此后 home
* 态不命中不绘制、demo 态 home 控件不命中。
*
* 滚动实现(关键:SDK 控件不透明——结构体对外不可见,App 不能读/写控件坐标;也无
* 位置 setter):
* - 内容控件以**绝对坐标**创建(App 侧另存 rect_t 几何用于局部 flush/命中)。
* - 页滚动 = 按「base_y - scroll」**重建**内容控件(仍处于 ACT_DEMO,owner 不变);
* 各控件跨重建状态存于 g_ds(App 持有),重建后恢复。
* - LazyColumn 是第二滚动区:滚它时 meow_list_scroll_by + 列表区局部刷新,不重建。
* - 因内容仅略超一屏(max_scroll 小),一次滑动仅触发 1~2 次重建,代价可接受。
*
* 宿主/qemu 离屏:设 MEOW_UI_DUMP=<ppm 路径> 时,首帧 flush 即把 back_buf 写成
* PPM 截图供检查;设备上无副作用。
@@ -28,16 +50,17 @@
#include <unistd.h>
#include <time.h>
/* §6.1 ①:Activity 归属。主界面(根,stack 空时 active()=0)绑定全部按钮控件;
* 全屏沉浸页为独立 Activity(无控件)——触摸它绝不命中主界面按钮(根治跨页残留)。
* 进全屏 meow_ui_activity_push(ACT_FULL),退出 pop 回根(主界面按钮恢复可点,对象复用)。 */
/* §6.1 ①:Activity 归属。home=根(0);全屏=独立 Activity(无控件);demo=独立 Activity。 */
#define ACT_HOME 0
#define ACT_FULL 1
#define ACT_DEMO 2
/* ---- home 既有控件 ---- */
static meow_widget *g_btn;
static meow_widget *g_nbtn[6]; /* 通知触发按钮行 */
static meow_widget *g_cancel; /* 取消最近一条通知 */
static meow_widget *g_fs_btn; /* FULLSCREEN 进入按钮(主界面) */
static meow_widget *g_demo_btn; /* 「控件库 Demo」入口按钮(主界面,新增) */
static int g_fullscreen = 0; /* 1=处于全屏视图(点任意处退出回主界面) */
static const char *g_notif_labels[6] = { "INFO", "MEDIA", "HIGH", "PROG", "ONGO", "TGT" };
static uint32_t g_last_notif = 0; /* 最近一次 send 返回的 id,供 cancel */
@@ -45,72 +68,107 @@ static int g_taps = 0;
static int g_secs = 0;
static char g_btn_label[32] = "TAP ME";
/* M2.51:统一日志改用 SDK meow_log()(按 app 分离 + 级别过滤 + 降级),
* 删除自造轮子的 hellolog()(自读 MEOW_LOG_DIR + snprintf + fopen/fprintf)。 */
/* ---- demo 演示页状态 ---- */
static int g_demo = 0; /* 1=处于 demo 演示页 */
static int g_demo_scroll = 0; /* 内容区滚动偏移(base 空间 px) */
static int g_demo_focus = 0; /* 1=LazyColumn 聚焦(swipe 滚列表而非页面) */
static int g_demo_fab = 0; /* FAB 点击计数反馈 */
static int g_demo_prog = 0; /* ProgressBar 动画当前值 0..100 */
static uint64_t g_demo_prog_last = 0;
static int g_demo_ct = 0; /* 内容视口顶(屏幕 y,= statusbar+56+8) */
static int g_demo_cb = 0; /* 内容视口底(屏幕 y,=608,NavBar 上方留白) */
static int g_demo_content_h = 0; /* 内容总高(base 空间;决定 max_scroll) */
/* SDK 控件不透明:App 自记几何,用于局部 flush / 命中 / scroll 重建。 */
typedef struct { int x, y, w, h; } rect_t;
#define RX(x,y,w,h) ((rect_t){ (x), (y), (w), (h) })
/* demo 各控件跨重建保留的状态(app 持有;重建后按此恢复) */
static struct {
int sw_on; int slider; char tf[32];
int ck; int r1, r2; int btn[5]; int card; int tab; int list; int prog;
} g_ds = { .sw_on = 1, .slider = 40, .r1 = 1, .tab = 1, .list = 0, .prog = 0 };
/* demo 内容布局常量(base 空间) */
#define DCL_X 16
#define DCR_X 252
#define DCW 212
#define DFW 448
#define DBH 44
#define DCH 52 /* 一行两列按钮步进 */
/* demo 内容槽:控件 + 其当前屏幕几何(rebuild 时重算) */
#define D_SLOT_MAX 64
static struct { meow_widget *w; rect_t r; } g_dslot[D_SLOT_MAX];
static int g_dslot_n = 0;
/* demo 分区小标题(纯文本,非控件;屏幕坐标在 rebuild 时算好) */
#define D_TITLE_MAX 16
static struct { int x, y; const char *s; } g_dtitle[D_TITLE_MAX];
static int g_dtitle_n = 0;
/* demo 交互组件引用(rebuild 时 g_* 与 r_* 同步重指/recompute) */
static meow_widget *g_tb; static rect_t r_tb; /* TopAppBar(chrome 固定) */
static meow_widget *g_nav; static rect_t r_nav; /* NavigationBar(chrome 固定) */
static meow_widget *g_fab; static rect_t r_fab; /* FAB(chrome 固定,可重建换计数 label) */
static meow_widget *g_dbtn_b[5]; static rect_t r_btn[5]; /* Button 五型 */
static meow_widget *g_dbtn_d; /* DISABLED 按钮(禁用不命中,无需 rect) */
static meow_widget *g_dsw; static rect_t r_dsw; /* Switch */
static meow_widget *g_dslider; static rect_t r_slider, r_slider_txt; /* Slider + 值文本 */
static meow_widget *g_dtf; static rect_t r_dtf; /* TextField */
static meow_widget *g_dcb; static rect_t r_dcb; /* Checkbox */
static meow_widget *g_dr1, *g_dr2; static rect_t r_dr1, r_dr2; /* Radio 单选组 */
static meow_widget *g_dprl; static rect_t r_prl; /* ProgressBar 线性 */
static meow_widget *g_dprc; static rect_t r_prc; /* ProgressBar 圆形 */
static meow_widget *g_dcard; static rect_t r_card; /* Card */
static meow_widget *g_dcard_btn; static rect_t r_cardbtn; /* Card 内容子按钮 */
static meow_widget *g_dbadge; /* Badge */
static meow_widget *g_dtab; static rect_t r_tab; /* TabBar */
static meow_widget *g_dbtn_dlg; static rect_t r_dlg; /* 打开 Dialog 的按钮 */
static meow_widget *g_dbtn_sheet; static rect_t r_sheet;/* 打开 BottomSheet 的按钮 */
static meow_widget *g_dlc; static rect_t r_list; /* LazyColumn */
/* Dialog / BottomSheet(阻塞模态;非控件,main() 创建一次) */
static meow_dialog *g_dlg;
static meow_sheet *g_sheet;
/* 前向声明 */
static void redraw(void);
static void demo_rebuild_to(int scroll);
static void demo_paint(void);
/* M2.51:统一日志改用 SDK meow_log()。 */
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
/* 通知触发:对应第 which 个按钮(0..5)。SDK 端 meow_notify_send() 自动注册未声明
* 类型(本应用用到 4 种:GENERAL / MEDIA / PROGRESS / MESSAGE,均在 4 种上限内),
* 超时语义见 meow_ui.h §9.1;媒体类型为单例(重发替换)。 */
/* ============================================================ */
/* 通知触发(home 既有) */
/* ============================================================ */
static void notif_send(int which)
{
meow_notify n;
memset(&n, 0, sizeof n);
n.progress = -1; /* 缺省不显示进度条 */
n.progress = -1;
n.timeout_ms = MEOW_NOTIFY_TIMEOUT_NEVER;
n.urgency = MEOW_NOTIFY_URG_NORMAL;
const char *label = "?";
switch (which) {
case 0: /* 普通 INFO */
label = "INFO";
n.cat = MEOW_NOTIFY_CAT_GENERAL;
n.title = "测试-普通通知";
n.body = "普通 INFO 通知,timeout=5000ms";
n.timeout_ms = 5000;
break;
case 1: /* 媒体单例(ongoing,不自动过期) */
label = "MEDIA";
n.cat = MEOW_NOTIFY_CAT_MEDIA;
n.urgency = MEOW_NOTIFY_URG_LOW;
n.title = "测试-媒体通知";
n.body = "媒体单例,正在播放,timeout=-1 常驻";
n.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING;
break;
case 2: /* HIGH 紧急度 */
label = "HIGH";
n.cat = MEOW_NOTIFY_CAT_GENERAL;
n.urgency = MEOW_NOTIFY_URG_HIGH;
n.title = "测试-紧急度HIGH";
n.body = "高紧急度:顶部横幅+提示音";
n.timeout_ms = 3000;
break;
case 3: /* 进度通知 */
label = "PROG";
n.cat = MEOW_NOTIFY_CAT_PROGRESS;
n.title = "测试-进度通知";
n.body = "下载进度 40%";
n.timeout_ms = 8000;
n.progress = 40;
break;
case 4: /* 常驻(timeout=-1,不自动过期,需手动取消) */
label = "ONGO";
n.cat = MEOW_NOTIFY_CAT_GENERAL;
n.title = "测试-常驻通知";
n.body = "常驻不自动过期(timeout=-1),需手动取消";
n.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING;
break;
case 5: /* 带 target_activity + icon(SDK 结构含 icon 字段;NULL/空=用 app 默认图标) */
label = "TGT";
n.cat = MEOW_NOTIFY_CAT_MESSAGE;
n.title = "测试-跳转Activity";
n.body = "点击跳转 target_activity=main";
n.timeout_ms = 6000;
n.target_activity = "main";
n.icon = NULL; /* 空 → launcher 用 app 默认图标(app.json icon) */
break;
default:
return;
case 0: label = "INFO"; n.cat = MEOW_NOTIFY_CAT_GENERAL;
n.title = "测试-普通通知"; n.body = "普通 INFO 通知,timeout=5000ms"; n.timeout_ms = 5000; break;
case 1: label = "MEDIA"; n.cat = MEOW_NOTIFY_CAT_MEDIA; n.urgency = MEOW_NOTIFY_URG_LOW;
n.title = "测试-媒体通知"; n.body = "媒体单例,正在播放,timeout=-1 常驻";
n.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING; break;
case 2: label = "HIGH"; n.cat = MEOW_NOTIFY_CAT_GENERAL; n.urgency = MEOW_NOTIFY_URG_HIGH;
n.title = "测试-紧急度HIGH"; n.body = "高紧急度:顶部横幅+提示音"; n.timeout_ms = 3000; break;
case 3: label = "PROG"; n.cat = MEOW_NOTIFY_CAT_PROGRESS;
n.title = "测试-进度通知"; n.body = "下载进度 40%"; n.timeout_ms = 8000; n.progress = 40; break;
case 4: label = "ONGO"; n.cat = MEOW_NOTIFY_CAT_GENERAL;
n.title = "测试-常驻通知"; n.body = "常驻不自动过期(timeout=-1),需手动取消";
n.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING; break;
case 5: label = "TGT"; n.cat = MEOW_NOTIFY_CAT_MESSAGE;
n.title = "测试-跳转Activity"; n.body = "点击跳转 target_activity=main";
n.timeout_ms = 6000; n.target_activity = "main"; n.icon = NULL; break;
default: return;
}
uint32_t id = meow_notify_send(&n);
@@ -119,8 +177,7 @@ static void notif_send(int which)
"notify btn=%s id=%u cat=%s urg=%s tmo=%d prog=%d target=%s title=%s",
label, (unsigned)id, meow_notify_category_str(n.cat),
meow_notify_urgency_str(n.urgency), n.timeout_ms, n.progress,
n.target_activity ? n.target_activity : "-",
n.title ? n.title : "-");
n.target_activity ? n.target_activity : "-", n.title ? n.title : "-");
}
static void notif_cancel(void)
@@ -130,13 +187,6 @@ static void notif_cancel(void)
g_last_notif = 0;
}
/* 进入/退出全屏视图(MeowOS 全屏机制,§3.1):
* - fullscreen(1) + collapsed(1):状态栏「上收出屏只留提示线」(不完全沉浸,inset=4px),
* 顶部提示线热区仍可下拉,左缘返回手势(app 已注册 on_swipe)仍生效——SDK 自动处理,非 app 拦截;
* - fullscreen(0) + collapsed(0):恢复整条状态栏主界面。
* SDK 基座默认瞬切(g_sbar_collapse_anim_ms=0);如需过渡动画可先 meow_ui_statusbar_animate(ms)。
* §6.1 ①:全屏 = 独立 Activity——进时压栈 ACT_FULL(主界面控件因 owner=ACT_HOME 不再参与命中,
* 根治「全屏页触摸到主页按钮区域闪现」);退时弹栈回根,主界面按钮恢复可点(对象复用零重建)。 */
static void set_fullscreen(int on)
{
g_fullscreen = on ? 1 : 0;
@@ -145,16 +195,315 @@ static void set_fullscreen(int on)
if (on) meow_ui_activity_push(ACT_FULL);
else meow_ui_activity_pop();
meow_log(MEOW_LOG_INFO, g_fullscreen ? "fullscreen ON (collapsed), activity->%d"
: "fullscreen OFF (restore), activity->%d",
: "fullscreen OFF (restore), activity->%d",
meow_ui_activity_active());
}
/* ============================================================ */
/* demo 演示页:布局 / 绘制 / 交互 */
/* ============================================================ */
static void d_slot(meow_widget *w, rect_t r)
{
if (g_dslot_n < D_SLOT_MAX) { g_dslot[g_dslot_n].w = w; g_dslot[g_dslot_n].r = r; g_dslot_n++; }
}
static void d_title(int x, int y, const char *s)
{
if (g_dtitle_n < D_TITLE_MAX) { g_dtitle[g_dtitle_n].x = x; g_dtitle[g_dtitle_n].y = y;
g_dtitle[g_dtitle_n].s = s; g_dtitle_n++; }
}
/* LazyColumn 数据源(100 项虚拟化) */
static int demo_list_src(int idx, char *label, int len, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= 100) return 0;
snprintf(label, (size_t)len, "list item %d", idx);
return 1;
}
/* 释放当前全部内容控件(含 Card 的独立子按钮);chrome 不在其列。 */
static void demo_free_content(void)
{
for (int i = 0; i < g_dslot_n; i++) meow_widget_free(g_dslot[i].w);
g_dslot_n = 0;
if (g_dcard_btn) { meow_widget_free(g_dcard_btn); g_dcard_btn = NULL; }
g_dtitle_n = 0;
}
/* 按 scroll 重建全部内容控件(owner=ACT_DEMO:调用方须已 push ACT_DEMO)。
* 控件以绝对坐标创建于 (x, base_y - scroll),几何同步写入 r_* / 槽;状态从 g_ds 恢复。 */
static void demo_build_at(int scroll)
{
int ct = meow_ui_statusbar_height() + 56 + 8;
g_demo_ct = ct; g_demo_cb = 608;
g_dslot_n = 0; g_dtitle_n = 0;
int yb = 0;
#define SY(base) (ct + (base) - scroll) /* base 空间 → 屏幕 y */
#define SLOT(w,x,y,ww,hh) do { rect_t _r=RX(x,y,ww,hh); d_slot((w),_r); } while(0)
/* ---- Buttons(五型 + 禁用) ---- */
d_title(DCL_X, SY(yb), "BUTTONS 5-variants"); yb += 26;
r_btn[0] = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_b[0] = meow_btn_create_variant(r_btn[0].x, r_btn[0].y, DCW, DBH, "FILLED", MEOW_BTN_FILLED); SLOT(g_dbtn_b[0], r_btn[0].x, r_btn[0].y, r_btn[0].w, r_btn[0].h);
r_btn[1] = RX(DCR_X, SY(yb), DCW, DBH); g_dbtn_b[1] = meow_btn_create_variant(r_btn[1].x, r_btn[1].y, DCW, DBH, "TONAL", MEOW_BTN_TONAL); SLOT(g_dbtn_b[1], r_btn[1].x, r_btn[1].y, r_btn[1].w, r_btn[1].h);
yb += DCH;
r_btn[2] = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_b[2] = meow_btn_create_variant(r_btn[2].x, r_btn[2].y, DCW, DBH, "OUTLINED", MEOW_BTN_OUTLINED); SLOT(g_dbtn_b[2], r_btn[2].x, r_btn[2].y, r_btn[2].w, r_btn[2].h);
r_btn[3] = RX(DCR_X, SY(yb), DCW, DBH); g_dbtn_b[3] = meow_btn_create_variant(r_btn[3].x, r_btn[3].y, DCW, DBH, "TEXT", MEOW_BTN_TEXT); SLOT(g_dbtn_b[3], r_btn[3].x, r_btn[3].y, r_btn[3].w, r_btn[3].h);
yb += DCH;
r_btn[4] = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_b[4] = meow_btn_create_variant(r_btn[4].x, r_btn[4].y, DCW, DBH, "ELEVATED", MEOW_BTN_ELEVATED); SLOT(g_dbtn_b[4], r_btn[4].x, r_btn[4].y, r_btn[4].w, r_btn[4].h);
g_dbtn_d = meow_btn_create_variant(DCR_X, SY(yb), DCW, DBH, "DISABLED", MEOW_BTN_FILLED);
SLOT(g_dbtn_d, DCR_X, SY(yb), DCW, DBH);
meow_widget_set_enabled(g_dbtn_d, 0); /* 禁用态:不命中 + 变暗 */
yb += DCH;
for (int i = 0; i < 5; i++) meow_widget_set_selected(g_dbtn_b[i], g_ds.btn[i]);
SLOT(meow_divider_create(DCL_X, SY(yb), DFW, 0), DCL_X, SY(yb), DFW, 1); yb += 16;
/* ---- Input / State(Switch / Slider / TextField / Checkbox / Radio) ---- */
d_title(DCL_X, SY(yb), "INPUT & STATE"); yb += 26;
r_dsw = RX(DCL_X, SY(yb), 56, 32); g_dsw = meow_switch_create(r_dsw.x, r_dsw.y, g_ds.sw_on); SLOT(g_dsw, r_dsw.x, r_dsw.y, r_dsw.w, r_dsw.h); yb += 44;
r_slider_txt = RX(DCL_X, SY(yb), 100, 24); yb += 24;
r_slider = RX(DCL_X, SY(yb), DFW, 40); g_dslider = meow_slider_create(r_slider.x, r_slider.y, r_slider.w, 0, 100, g_ds.slider); SLOT(g_dslider, r_slider.x, r_slider.y, r_slider.w, r_slider.h); yb += 48;
r_dtf = RX(DCL_X, SY(yb), DFW, 44); g_dtf = meow_textfield_create(r_dtf.x, r_dtf.y, r_dtf.w, r_dtf.h, "Type here...", g_ds.tf, 32, MEOW_IME_ASCII); SLOT(g_dtf, r_dtf.x, r_dtf.y, r_dtf.w, r_dtf.h); yb += 54;
r_dcb = RX(DCL_X, SY(yb), 34, 34); g_dcb = meow_checkbox_create(r_dcb.x, r_dcb.y); SLOT(g_dcb, r_dcb.x, r_dcb.y, r_dcb.w, r_dcb.h);
r_dr1 = RX(DCR_X, SY(yb), 34, 34); g_dr1 = meow_radio_create(r_dr1.x, r_dr1.y); SLOT(g_dr1, r_dr1.x, r_dr1.y, r_dr1.w, r_dr1.h);
yb += 40;
r_dr2 = RX(DCR_X, SY(yb), 34, 34); g_dr2 = meow_radio_create(r_dr2.x, r_dr2.y); SLOT(g_dr2, r_dr2.x, r_dr2.y, r_dr2.w, r_dr2.h);
yb += 40;
meow_widget_set_selected(g_dcb, g_ds.ck);
meow_widget_set_selected(g_dr1, g_ds.r1);
meow_widget_set_selected(g_dr2, g_ds.r2);
/* ---- ProgressBar(线性 + 圆形,on_tick 动画) ---- */
d_title(DCL_X, SY(yb), "PROGRESS (animated)"); yb += 26;
r_prl = RX(DCL_X, SY(yb), DFW, 8); g_dprl = meow_progress_create(r_prl.x, r_prl.y, r_prl.w, r_prl.h, g_ds.prog); SLOT(g_dprl, r_prl.x, r_prl.y, r_prl.w, r_prl.h); yb += 16;
r_prc = RX(DCL_X + 400, SY(yb), 34, 34); g_dprc = meow_progress_circular_create(r_prc.x + 17, r_prc.y + 17, 17, g_ds.prog); SLOT(g_dprc, r_prc.x, r_prc.y, r_prc.w, r_prc.h); yb += 48;
/* ---- Container(Card / Badge / TabBar) ---- */
d_title(DCL_X, SY(yb), "CONTAINER"); yb += 26;
r_card = RX(DCL_X, SY(yb), DFW, 88); g_dcard = meow_card_create(r_card.x, r_card.y, r_card.w, r_card.h); SLOT(g_dcard, r_card.x, r_card.y, r_card.w, r_card.h);
r_cardbtn = RX(DCL_X + 16, SY(yb) + 20, DFW - 32, 40); g_dcard_btn = meow_btn_create_variant(r_cardbtn.x, r_cardbtn.y, r_cardbtn.w, r_cardbtn.h, "IN-CARD BUTTON", MEOW_BTN_TONAL);
meow_widget_set_selected(g_dcard_btn, g_ds.card);
meow_card_set_content(g_dcard, g_dcard_btn);
yb += 96;
g_dbadge = meow_badge_create(DCL_X + 60, SY(yb) + 10);
SLOT(g_dbadge, DCL_X + 52, SY(yb) + 2, 18, 18);
meow_badge_set(g_dbadge, 3);
yb += 30;
r_tab = RX(DCL_X, SY(yb), DFW, 36); g_dtab = meow_tabbar_create(r_tab.x, r_tab.y, r_tab.w, r_tab.h); SLOT(g_dtab, r_tab.x, r_tab.y, r_tab.w, r_tab.h);
meow_tabbar_add_tab(g_dtab, "Playlist");
meow_tabbar_add_tab(g_dtab, "Queue");
meow_tabbar_add_tab(g_dtab, "Now");
meow_tabbar_set_selected(g_dtab, g_ds.tab);
yb += 44;
/* ---- Modals(Dialog + BottomSheet) ---- */
d_title(DCL_X, SY(yb), "MODALS (blocking)"); yb += 26;
r_dlg = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_dlg = meow_btn_create_variant(r_dlg.x, r_dlg.y, r_dlg.w, r_dlg.h, "DIALOG", MEOW_BTN_OUTLINED); SLOT(g_dbtn_dlg, r_dlg.x, r_dlg.y, r_dlg.w, r_dlg.h);
r_sheet = RX(DCR_X, SY(yb), DCW, DBH); g_dbtn_sheet = meow_btn_create_variant(r_sheet.x, r_sheet.y, r_sheet.w, r_sheet.h, "SHEET", MEOW_BTN_OUTLINED); SLOT(g_dbtn_sheet, r_sheet.x, r_sheet.y, r_sheet.w, r_sheet.h);
yb += DCH;
/* ---- LazyColumn(100 项列表滚动) ---- */
d_title(DCL_X, SY(yb), "LIST (LazyColumn, tap+swipe)"); yb += 26;
r_list = RX(DCL_X, SY(yb), DFW, 150); g_dlc = meow_lazycolumn_create(r_list.x, r_list.y, r_list.w, r_list.h, 40, 100, demo_list_src, NULL);
SLOT(g_dlc, r_list.x, r_list.y, r_list.w, r_list.h);
meow_list_set_scroll(g_dlc, g_ds.list);
yb += 158;
g_demo_content_h = yb;
#undef SY
#undef SLOT
}
/* demo 全页重绘:背景 + 内容控件(已定位) + 标题 + slider 值 + chrome(后画覆盖) + 内容区 flush */
static void demo_paint(void)
{
const meow_theme *th = meow_theme_get();
int sb = meow_ui_statusbar_height();
meow_ui_fill_rect(0, sb, 480, 720 - sb, th->bg);
for (int i = 0; i < g_dslot_n; i++) meow_widget_draw(g_dslot[i].w);
for (int i = 0; i < g_dtitle_n; i++)
meow_ui_text(g_dtitle[i].x, g_dtitle[i].y, g_dtitle[i].s, 20, th->primary);
if (g_dslider) {
char buf[28]; snprintf(buf, sizeof buf, "slider = %d", meow_slider_get(g_dslider));
meow_ui_text(r_slider_txt.x, r_slider_txt.y, buf, 20, th->dim);
}
/* chrome(固定;后画覆盖内容溢出区) */
meow_widget_draw(g_tb);
meow_widget_draw(g_nav);
meow_widget_draw(g_fab);
meow_ui_flush_region(0, sb, 480, 720 - sb);
}
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用 */
static void demo_rebuild_to(int scroll)
{
int vp_h = g_demo_cb - g_demo_ct;
int maxs = g_demo_content_h - vp_h;
if (maxs < 0) maxs = 0;
if (scroll < 0) scroll = 0;
if (scroll > maxs) scroll = maxs;
demo_free_content();
demo_build_at(scroll);
g_demo_scroll = scroll;
demo_paint();
}
/* 局部刷新一个控件:清底 + 重画 + flush 其几何矩形(App 自记 rect) */
static void d_redraw(meow_widget *w, rect_t r)
{
if (!w) return;
const meow_theme *th = meow_theme_get();
meow_ui_fill_rect(r.x, r.y, r.w, r.h, th->bg);
meow_widget_draw(w);
meow_ui_flush_region(r.x, r.y, r.w, r.h);
}
static void d_toggle_sel(meow_widget *w, rect_t r)
{
if (!w) return;
meow_widget_set_selected(w, !meow_widget_selected(w));
d_redraw(w, r);
}
/* Tap:落点在内容控件上,返回 1=已消费 */
static int demo_content_tap(int x, int y)
{
g_demo_focus = 0; /* 默认切回页滚动;仅命中列表时改置 1 */
for (int i = 0; i < 5; i++)
if (meow_widget_hit(g_dbtn_b[i], x, y)) { d_toggle_sel(g_dbtn_b[i], r_btn[i]); g_ds.btn[i] = meow_widget_selected(g_dbtn_b[i]); return 1; }
if (meow_widget_hit(g_dr1, x, y)) { meow_widget_set_selected(g_dr1, 1); meow_widget_set_selected(g_dr2, 0); g_ds.r1 = 1; g_ds.r2 = 0; d_redraw(g_dr1, r_dr1); d_redraw(g_dr2, r_dr2); return 1; }
if (meow_widget_hit(g_dr2, x, y)) { meow_widget_set_selected(g_dr2, 1); meow_widget_set_selected(g_dr1, 0); g_ds.r2 = 1; g_ds.r1 = 0; d_redraw(g_dr1, r_dr1); d_redraw(g_dr2, r_dr2); return 1; }
if (meow_widget_hit(g_dcb, x, y)) { d_toggle_sel(g_dcb, r_dcb); g_ds.ck = meow_widget_selected(g_dcb); return 1; }
if (meow_widget_hit(g_dsw, x, y)) { g_ds.sw_on = !meow_switch_get(g_dsw); meow_switch_set(g_dsw, g_ds.sw_on); d_redraw(g_dsw, r_dsw); return 1; }
if (meow_widget_hit(g_dslider, x, y)) {
int frac = x - r_slider.x; if (frac < 0) frac = 0; if (frac > r_slider.w) frac = r_slider.w;
g_ds.slider = frac * 100 / r_slider.w;
meow_slider_set(g_dslider, g_ds.slider);
d_redraw(g_dslider, r_slider);
{
const meow_theme *th = meow_theme_get();
char buf[28]; snprintf(buf, sizeof buf, "slider = %d", g_ds.slider);
meow_ui_fill_rect(r_slider_txt.x, r_slider_txt.y, r_slider_txt.w, r_slider_txt.h, th->bg);
meow_ui_text(r_slider_txt.x, r_slider_txt.y, buf, 20, th->dim);
meow_ui_flush_region(r_slider_txt.x, r_slider_txt.y, r_slider_txt.w, r_slider_txt.h);
}
return 1;
}
if (meow_widget_hit(g_dtf, x, y)) {
meow_textfield_activate(g_dtf); /* 阻塞 IME,返回后刷新 + 状态回写 */
strncpy(g_ds.tf, meow_textfield_text(g_dtf), sizeof g_ds.tf - 1);
g_ds.tf[sizeof g_ds.tf - 1] = 0;
d_redraw(g_dtf, r_dtf);
return 1;
}
if (meow_widget_hit(g_dcard_btn, x, y)) { d_toggle_sel(g_dcard_btn, r_cardbtn); g_ds.card = meow_widget_selected(g_dcard_btn); return 1; }
if (g_dtab) {
int t = meow_tabbar_hit(g_dtab, x, y);
if (t >= 0) { g_ds.tab = t; meow_tabbar_set_selected(g_dtab, t); d_redraw(g_dtab, r_tab); return 1; }
}
if (meow_widget_hit(g_dbtn_dlg, x, y)) { meow_dialog_show(g_dlg); meow_log(MEOW_LOG_INFO, "dialog result=%d", meow_dialog_result(g_dlg)); d_redraw(g_dbtn_dlg, r_dlg); return 1; }
if (meow_widget_hit(g_dbtn_sheet, x, y)) { int r = meow_sheet_open(g_sheet); meow_log(MEOW_LOG_INFO, "sheet result=%d", r); d_redraw(g_dbtn_sheet, r_sheet); return 1; }
if (meow_widget_hit(g_dlc, x, y)) { g_demo_focus = 1; return 1; }
return 0;
}
/* demo 页 on_tap 分发(chrome → 内容) */
static void demo_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { g_demo = 0; meow_ui_activity_pop(); redraw(); return; }
}
if (g_fab && meow_widget_hit(g_fab, x, y)) {
g_demo_fab++;
char lb[16]; snprintf(lb, sizeof lb, "+%d", g_demo_fab);
meow_widget_free(g_fab);
g_fab = meow_fab_create(r_fab.x, r_fab.y, 56, lb);
meow_widget_draw(g_fab);
meow_ui_flush_region(r_fab.x, r_fab.y, r_fab.w, r_fab.h);
return;
}
if (g_nav) {
int idx = meow_navigationbar_hit(g_nav, x, y);
if (idx >= 0) { meow_navigationbar_set_selected(g_nav, idx); d_redraw(g_nav, r_nav); return; }
}
if (demo_content_tap(x, y)) return;
}
/* demo 页 on_tick:ProgressBar 动画(局部刷,~120ms 步进) */
static void demo_tick(uint64_t now)
{
if (g_demo_prog_last == 0) g_demo_prog_last = now;
if (now - g_demo_prog_last < 120) return;
g_demo_prog_last = now;
g_ds.prog = (g_ds.prog + 3) % 101;
g_demo_prog = g_ds.prog;
meow_progress_set(g_dprl, g_demo_prog); d_redraw(g_dprl, r_prl);
meow_progress_set(g_dprc, g_demo_prog); d_redraw(g_dprc, r_prc);
}
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表聚焦滚列表,否则页滚动 */
static void demo_swipe(int dir, int dist)
{
if (dir == MEOW_SWIPE_LEFT) { g_demo = 0; meow_ui_activity_pop(); redraw(); return; }
int del = 0;
if (dir == MEOW_SWIPE_UP) del = dist; /* 手指上移 → 偏移增大 */
else if (dir == MEOW_SWIPE_DOWN) del = -dist;
if (del == 0) return;
if (g_demo_focus && g_dlc) {
meow_list_scroll_by(g_dlc, del);
g_ds.list = meow_list_scroll(g_dlc);
d_redraw(g_dlc, r_list); /* 列表区局部刷新 */
} else {
demo_rebuild_to(g_demo_scroll + del);
}
}
/* 进入 demo 演示页(重建到顶部) */
static void demo_enter(void)
{
g_demo = 1;
g_demo_focus = 0;
meow_ui_activity_push(ACT_DEMO);
demo_rebuild_to(0);
meow_log(MEOW_LOG_INFO, "demo enter scroll=0");
}
/* 启动时创建 chrome(固定)+ 内容(scroll=0);push(ACT_DEMO) 后创建使 owner=ACT_DEMO。 */
static void demo_init(void)
{
int sb = meow_ui_statusbar_height();
meow_ui_activity_push(ACT_DEMO);
r_tb = RX(0, sb, 480, 56); g_tb = meow_topappbar_create(0, sb, 480, 56, "Widgets Demo");
meow_topappbar_set_back(g_tb, 1);
r_nav = RX(0, 680, 480, 40); g_nav = meow_navigationbar_create(0, 680, 480, 40);
meow_navigationbar_add_item(g_nav, "Buttons");
meow_navigationbar_add_item(g_nav, "List");
meow_navigationbar_add_item(g_nav, "More");
meow_navigationbar_set_selected(g_nav, 0);
r_fab = RX(406, 616, 56, 56); g_fab = meow_fab_create(406, 616, 56, "+");
demo_build_at(0);
meow_ui_activity_pop();
g_demo_scroll = 0;
}
/* ============================================================ */
/* 既有 home 绘制 / 事件 */
/* ============================================================ */
static void redraw(void)
{
const meow_theme *th = meow_theme_get();
int sb = meow_ui_statusbar_height(); /* A-0 全局状态栏 inset:内容下移避免被盖 */
int sb = meow_ui_statusbar_height();
if (g_fullscreen) {
/* 全屏页:纯色占满 + 居中提示;点任意处 / 左缘左滑(SDK on_swipe)返回 */
meow_ui_clear(th->accent);
meow_ui_text_center(240, 330, "FULLSCREEN ACTIVE", 28, 0xFFFFFF);
meow_ui_text_center(240, 366, "tap anywhere = back to home", 20, 0xFFFFFF);
@@ -162,8 +511,6 @@ static void redraw(void)
meow_ui_flush();
return;
}
/* inset:只清「状态栏之下」内容区(top sb px 保留给系统状态栏——由 launcher 进程叠加绘制;
* App 进程不渲染状态栏、也不冲掉它)。非全屏时 App 内容只在 y>=sb。 */
if (sb > 0) meow_ui_fill_rect(0, sb, 480, 720 - sb, th->bg);
else meow_ui_clear(th->bg);
meow_ui_text_center(240, sb + 8, "MEOWOS HELLO", 28, th->fg);
@@ -173,6 +520,7 @@ static void redraw(void)
for (int i = 0; i < 6; i++) meow_widget_draw(g_nbtn[i]);
meow_widget_draw(g_cancel);
meow_widget_draw(g_fs_btn);
meow_widget_draw(g_demo_btn);
char line[64];
snprintf(line, sizeof line, "taps=%d uptime=%ds", g_taps, g_secs);
meow_ui_text_center(240, 516, line, 20, th->dim);
@@ -182,33 +530,17 @@ static void redraw(void)
static void on_tap(int x, int y)
{
if (g_fullscreen) {
/* 全屏页:点任意处退出回主界面(左缘左滑由 SDK on_swipe 处理,见下) */
set_fullscreen(0);
redraw();
return;
}
if (g_demo) { demo_on_tap(x, y); return; }
if (g_fullscreen) { set_fullscreen(0); redraw(); return; }
if (meow_widget_hit(g_demo_btn, x, y)) { demo_enter(); return; }
for (int i = 0; i < 6; i++) {
if (meow_widget_hit(g_nbtn[i], x, y)) {
notif_send(i);
redraw();
return;
}
}
if (meow_widget_hit(g_cancel, x, y)) {
notif_cancel();
redraw();
return;
}
if (meow_widget_hit(g_fs_btn, x, y)) {
set_fullscreen(1);
redraw();
return;
if (meow_widget_hit(g_nbtn[i], x, y)) { notif_send(i); redraw(); return; }
}
if (meow_widget_hit(g_cancel, x, y)) { notif_cancel(); redraw(); return; }
if (meow_widget_hit(g_fs_btn, x, y)) { set_fullscreen(1); redraw(); return; }
if (meow_widget_hit(g_btn, x, y)) {
g_taps++;
snprintf(g_btn_label, sizeof g_btn_label, "TAPPED x%d", g_taps);
/* 重建按钮标签(release 旧控件,重新 create 保持几何) */
meow_widget_free(g_btn);
g_btn = meow_btn_create(120, 300, 240, 64, g_btn_label);
redraw();
@@ -217,26 +549,20 @@ static void on_tap(int x, int y)
static void on_key(int keycode, int is_down)
{
/* §3.4.1:物理键语义由系统统一管理,APP 禁映射(电源=息屏/亮屏、音量=调音量、播放=媒体)。
* 这里仅记录日志,不做任何 exit/confirm 行为。 */
(void)keycode; (void)is_down;
meow_log(MEOW_LOG_DEBUG, "key"); /* 不处理:SDK 内部消化系统键语义 */
meow_log(MEOW_LOG_DEBUG, "key");
}
static void on_swipe_progress(int x_start, int dx)
{
/* 左滑过程实时回调:SDK 已自动画顶部进度条;这里演示 app 侧接入(画同长度条,幂等——
* 长度=当前 X=x_start+dx,与 SDK 自动画一致,不会误清条)。
* 抬手达标时 SDK 清条 + 触发 on_swipe(LEFT)→退出。 */
meow_ui_swipe_hint(x_start + dx);
}
static void on_swipe(int dir, int dx, int dy)
{
(void)dx; (void)dy;
(void)dy;
if (g_demo) { demo_swipe(dir, dx); return; }
if (dir == MEOW_SWIPE_LEFT) {
/* §6.1 导航栈语义:非根(全屏)→ 左滑返回主页(pop),不退出;根 → 退出回应用列表。
* 保证 on_swipe(LEFT) 在沉浸页=退出全屏回主页,而非直接退出整个 app。 */
if (g_fullscreen) { set_fullscreen(0); redraw(); }
else { meow_log(MEOW_LOG_INFO, "exit:swipe"); meow_ui_request_exit(); }
}
@@ -244,9 +570,9 @@ static void on_swipe(int dir, int dx, int dy)
static void on_tick(void)
{
/* 每秒刷新一次状态行(避免 16ms 高频重绘) */
static uint64_t last = 0;
uint64_t now = meow_ui_time_ms();
if (g_demo) { demo_tick(now); return; }
static uint64_t last = 0;
if (now - last >= 1000) {
last = now;
g_secs++;
@@ -259,26 +585,44 @@ int main(void)
meow_log(MEOW_LOG_INFO, "start");
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
/* M2.46 不再强制 set_mode 深色——主题跟随系统 display.theme/theme_mode(manual/auto+时段),
* 与 launcher s_ui_theme() 判定对齐(meow_theme_get() 已读 settings.conf)。 */
meow_theme_add_listener(NULL); /* noop: 预留主题变化重绘 */
meow_theme_add_listener(NULL);
signal(SIGTERM, on_sigterm); /* 看门狗返回键 → 干净退出 */
signal(SIGTERM, on_sigterm);
g_btn = meow_btn_create(120, 300, 240, 64, g_btn_label);
for (int i = 0; i < 6; i++)
g_nbtn[i] = meow_btn_create(3 + i * 80, 400, 74, 56, g_notif_labels[i]);
g_cancel = meow_btn_create(36, 464, 200, 42, "CANCEL LAST");
g_fs_btn = meow_btn_create(244, 464, 200, 42, "FULLSCREEN");
g_demo_btn = meow_btn_create(36, 560, 408, 44, "WIDGETS DEMO");
/* Dialog / BottomSheet 实例(阻塞模态;打开前排空残留触摸由 SDK 处理) */
g_dlg = meow_dialog_create(MEOW_DLG_SELECT, "Pick a fruit");
const char *items[] = { "Apple", "Banana", "Cherry" };
meow_dialog_set_items(g_dlg, items, 3, 1);
g_sheet = meow_sheet_create("Options", 0);
meow_sheet_add_item(g_sheet, "Copy");
meow_sheet_add_item(g_sheet, "Delete");
meow_sheet_add_item(g_sheet, "Share");
demo_init();
redraw();
meow_ui_events ev = { on_key, on_tap, on_swipe, on_tick, on_swipe_progress };
meow_ui_run(&ev);
meow_widget_free(g_demo_btn);
for (int i = 0; i < 6; i++) meow_widget_free(g_nbtn[i]);
meow_widget_free(g_cancel);
meow_widget_free(g_fs_btn);
meow_widget_free(g_btn);
/* demo 内容 + chrome */
demo_free_content();
meow_widget_free(g_tb);
meow_widget_free(g_nav);
meow_widget_free(g_fab);
meow_dialog_free(g_dlg);
meow_sheet_free(g_sheet);
meow_ui_shutdown();
meow_log(MEOW_LOG_INFO, "exit");
return 0;