diff --git a/patch_v2/files_app/README.md b/patch_v2/files_app/README.md index e83ecf0..87a15da 100644 --- a/patch_v2/files_app/README.md +++ b/patch_v2/files_app/README.md @@ -33,8 +33,10 @@ cd patch_v2/files_app && ./build_files.sh ``` 构建脚本仿 `hello_app/build_hello.sh`:`zig-mipsel-cc -Os -static -Wall -Wextra -I../sdk -I.` -链 `../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread`。**零触及 `patch_v2/sdk/`**(未新增/改任何 SDK 文件); -`-Wall -Wextra` 0 warning。 +链 `../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread`。本应用是 **§5.5 结构化列表(`meow_listview`)的 +首个消费方**(`patch_v2/sdk/meow_ui_widget.c` 新增 `W_LISTVIEW`:图标+标题+副标题+右侧操作+选中); +`-Wall -Wextra` 0 warning。行滚动/选中/命中由 SDK 收口,App 只驱动 scroll/sel 并分发(行体=进目录/ +开预览;右侧 `⋯` 或长按→BottomSheet)。 ## 部署 @@ -66,11 +68,14 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \ - 左缘左滑/返回钮:预览页→回目录;浏览器→上级目录;卡根再左滑=退出 App。 - 物理键不映射(APPS.md §3.4.1):息屏/音量/媒体语义归系统/SDK。 -## 已知限制 / SDK 能力缺口(未改 SDK,如需请走 agent 立项) +## 已知限制 / SDK 能力说明 -1. **SDK `meow_lazycolumn` 仅渲染单行 label**(`meow_list_source` 填文本),无法表达「图标+名称+大小/时间+右侧操作钮」 - 的行布局。本 App 用 `meow_widget_custom_create` 自绘虚拟化列表满足该行版式;`fsx_scan_dir` 仍以有界数组缓存 - (`FSX_MAX_ENTRIES=1024`)承载数据,列表虚拟化只画可见行(`fsx_visible_range`)——大目录不一次全读进内存。 +1. **结构化列表(`meow_listview`)已落地**:`ui/browser.c` 的自绘 custom 列表已迁移到 SDK `W_LISTVIEW` + (行数据源 `browser_src2` 填 `meow_list_item`)。图标(目录/文件内置符号)、名称、大小/时间副标题、 + 右侧 `⋯` 操作钮、虚拟化、空态/页脚、禁用行不响应均由 SDK 收口;`fsx_scan_dir` 仍以有界数组缓存 + (`FSX_MAX_ENTRIES=1024`),列表虚拟化只画可见行——大目录不一次全读进内存。 + - **long-press 由 App 层保持**:SDK listview 无内建长按;`browser_tick` 的「按下→≥500ms 未拖动→ + BottomSheet」逻辑不依赖控件渲染,保留在 App 层(触摸命中用 `meow_listview_hit` 行号)。 2. **中文字形**:SDK 默认 `common` 字库(≈4k)不含 CJK 全量;exFAT 中文文件名/中文文本预览在 common 档会显示缺字豆腐。 预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。 3. **预览仅文本**:按扩展名白名单 + 首 512 字节 NUL 嗅探判文本;二进制/图片无预览(提示「not a text file」)。 diff --git a/patch_v2/files_app/files b/patch_v2/files_app/files index 31b903b..ed947c8 100755 Binary files a/patch_v2/files_app/files and b/patch_v2/files_app/files differ diff --git a/patch_v2/files_app/ui/browser.c b/patch_v2/files_app/ui/browser.c index c1de528..2f02574 100644 --- a/patch_v2/files_app/ui/browser.c +++ b/patch_v2/files_app/ui/browser.c @@ -1,12 +1,15 @@ /* ui/browser.c — 目录浏览页(ACT_BROWSER,根页)view + 控制器。 * * 职责: - * - 持有目录态(g_cur 当前目录 / g_entries 缓存快照 / g_err 空态码 / g_scroll); - * - 内容区用 meow_widget_custom_create 自绘「虚拟化列表」:只画可见行(fsx_visible_range), - * 行=图标(目录/文件)+名称+大小/时间+右侧 ⋯ 操作钮;不一次全读进内存(entries 有界缓存); - * - 空态/错误(无卡/空目录/权限)由 render_cb 内 app_draw_empty 画; - * - 触摸:行 body=进目录/开预览;右侧操作钮或长按 → BottomSheet(删除/重命名/新建/属性); - * - 滑动:LEFT=返回上级(App 层处理);UP/DOWN=列表滚动(本页); + * - 持有目录态(g_cur 当前目录 / g_entries 缓存快照 / g_err 空态码); + * - 内容区用 SDK 结构化列表(meow_listview,§5.5 W_LISTVIEW)虚拟化渲染 + * 「图标 + 名称 + 大小/时间 + 右侧 ⋯ 操作钮」行:行数据源 browser_src2 逐行填充 + * meow_list_item(icon_kind=目录/文件符号、副标题=大小/时间、right=⋯)。滚动/选中命中由 + * SDK 收口(视口裁剪、局部 dirty、disabled 不响应),App 只在 on_tap/touch_move 驱动 + * scroll/sel 并分发:行体=进目录/开预览;右侧 ⋯ 或长按 → BottomSheet(删除/重命名/新建/属性); + * - 空态/错误(无卡/空目录/权限)与底部信息行用 meow_draw_custom_register 注册为非控件绘制物 + * (app_draw_empty / app_draw_footer)——列表视口不含页脚,行不压页脚;invalidate/paint_full + * 自动重画它们; * - 写操作只经 logic(fsx_*),且在卡内校验后执行。 * * 颜色全部读 meow_theme_get() 语义色(§8.1),不硬编码。 @@ -25,8 +28,6 @@ static fsx_entry g_entries[FSX_MAX_ENTRIES]; static int g_n; /* 当前目录项数 */ static char g_cur[FSX_MAX_PATH]; static int g_err; /* fsx_scan_dir 返回(0=ok,<0=空态码) */ -static int g_scroll; /* 列表滚动(px) */ -static int g_sel; /* 当前选中行(-1=无) */ static int g_press_row; /* 按下行(-1=无);长按检测 */ static uint64_t g_press_ms; static int g_press_tapped; /* 长按已消费本按 */ @@ -35,8 +36,11 @@ static int g_dragging; /* 本次触摸起点是否在列表(拖 static int g_drag_last_y; /* 拖拽上一帧 y */ static meow_widget *g_tb; /* TopAppBar(chrome) */ -static meow_widget *g_list; /* 内容区虚拟化列表(custom widget) */ -static app_rect_t r_content; +static meow_widget *g_list; /* 内容区结构化列表(meow_listview,W_LISTVIEW) */ +static app_rect_t r_content; /* 顶部栏之下 → 屏底(含页脚带) */ +static app_rect_t r_list; /* 列表视口 = 内容区去掉页脚带(行不压页脚) */ +static int g_empty_id; /* 空态/错误绘制物(非控件) */ +static int g_footer_id; /* 底部信息行绘制物(非控件) */ static char g_title[FSX_MAX_PATH]; /* 预览/操作辅助(前向声明) */ @@ -59,123 +63,65 @@ static void browser_drag_reset(void) g_drag_last_y = 0; } -/* ---- 通用文本绘制:宽度限 maxw 内截断(超出加 "...")---- */ -static void browser_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) +/* ---- 列表行数据源:逐行填 meow_list_item(图标/名称/大小时间/⋯ ),轻量不 IO(§5.5)---- */ +static int browser_src2(int idx, meow_list_item *out, void *ctx) { - int full = meow_ui_text_width(s, size); - if (full <= maxw) { meow_ui_text(x, y, s, size, col); return; } - const char *dots = "..."; - int dw = meow_ui_text_width(dots, size); - char tmp[FSX_MAX_NAME + 4]; - int i; - for (i = 0; s[i] && i < FSX_MAX_NAME - 4; i++) { - tmp[i] = s[i]; - tmp[i + 1] = '\0'; - if (meow_ui_text_width(tmp, size) > maxw - dw) break; - } - if (i >= FSX_MAX_NAME - 4) i = FSX_MAX_NAME - 4; - memcpy(tmp + i, dots, 4); - meow_ui_text(x, y, tmp, size, col); -} - -/* ---- 列表行图标:目录=filled 矩形+页签;文件=空心矩形+折角 ---- */ -static void browser_draw_icon(const meow_theme *th, int x, int y, int is_dir, int sel) -{ - uint32_t col = sel ? 0xFFFFFF : (is_dir ? th->primary : th->onSurfaceVariant); - if (is_dir) { - meow_ui_fill_rect(x, y + 6, 26, 18, col); /* 主体 */ - meow_ui_fill_rect(x, y + 2, 12, 6, col); /* 页签 */ + (void)ctx; + if (idx < 0 || idx >= g_n) return 0; + fsx_entry *e = &g_entries[idx]; + static char sub[FSX_MAX_NAME + 48]; /* 行内瞬时字符串(绘制即刻用,重复填充无碍) */ + char sz[32], tm[32]; + out->icon_kind = e->is_dir ? MEOW_LIST_ICON_FOLDER : MEOW_LIST_ICON_FILE; + out->title = e->name; + if (e->is_dir) { + fsx_fmt_time(e->mtime, tm, sizeof tm); + snprintf(sub, sizeof sub, "Folder %s", tm); } else { - meow_ui_draw_rect(x + 4, y + 4, 18, 22, col); /* 页体 */ - meow_ui_fill_rect(x + 4, y + 4, 7, 4, col); /* 文件名横条 */ + fsx_fmt_size(e->size, sz, sizeof sz); + fsx_fmt_time(e->mtime, tm, sizeof tm); + snprintf(sub, sizeof sub, "%s %s", sz, tm); } + out->subtitle = sub; + out->right = MEOW_LIST_RIGHT_MORE; /* 右侧 ⋯ 详情钮(行右 1/3 判操作) */ + out->on = 0; + out->enabled = 1; + return 1; } -/* 行区视口高(底部留页脚,行不压到页脚)。 */ -static int browser_vp_h(void) { return r_content.h - APP_FOOTER_H; } - -/* ---- 列表 render_cb:虚拟化画可见行 + 空态 + 页脚 ---- */ -static void browser_render(meow_widget *w, const meow_theme *th, void *ctx) +/* ---- 空态/错误绘制物(非控件;非空态时无输出)---- + * ⚠️ custom 绘制物无 owner 闸门(不同于 widget,见 meow_ui_draw.c draw_order): + * 必须显式按「本页是否为当前 active activity」守门,否则切到 viewer 页会跨页泄漏绘制。 */ +static void browser_draw_empty(void *ctx) { - (void)w; (void)ctx; - /* 空态/错误 */ - if (g_err != 0) { - if (g_err == FSX_ERR_NOENT) - app_draw_empty(r_content, "NO STORAGE CARD", "insert a TF card / mount sd_0"); - else if (g_err == FSX_ERR_EACCES) - app_draw_empty(r_content, "ACCESS DENIED", "cannot read this folder"); - else - app_draw_empty(r_content, "CANNOT OPEN", "unexpected error"); - return; - } - if (g_n == 0) { - app_draw_empty(r_content, "EMPTY FOLDER", g_cur); - app_draw_footer(r_content, "0 items"); - return; - } - - int vp = browser_vp_h(); - int first, last; - fsx_visible_range(g_n, APP_ROW_H, g_scroll, vp, &first, &last); - int thh = meow_ui_text_height(th->font_body); - int namew = r_content.w - APP_PAD - APP_ICON_W - APP_PAD - APP_OP_W - APP_PAD; /* 名称区 */ - char size[32], tm[32]; - - for (int i = first; i < last; i++) { - fsx_entry *e = &g_entries[i]; - int ry = r_content.y + (i * APP_ROW_H - g_scroll); - if (ry + APP_ROW_H <= r_content.y) continue; - if (ry >= r_content.y + vp) break; - - int hilite = (i == g_sel || i == g_press_row); - if (hilite) meow_ui_fill_rect(r_content.x, ry, r_content.w, APP_ROW_H, th->primary); - - browser_draw_icon(th, r_content.x + APP_PAD, ry + (APP_ROW_H - 24) / 2, e->is_dir, hilite); - - uint32_t namecol = hilite ? 0xFFFFFF : th->onSurface; - uint32_t subcol = hilite ? 0xFFFFFF : th->dim; - browser_text_trunc(r_content.x + APP_PAD + APP_ICON_W + APP_PAD, ry + 6, - e->name, namew, th->font_body, namecol); - - if (e->is_dir) { - fsx_fmt_time(e->mtime, tm, sizeof tm); - snprintf(size, sizeof size, "Folder %s", tm); - } else { - fsx_fmt_size(e->size, size, sizeof size); - fsx_fmt_time(e->mtime, tm, sizeof tm); - snprintf(size, sizeof size, "%s %s", size, tm); - } - browser_text_trunc(r_content.x + APP_PAD + APP_ICON_W + APP_PAD, ry + 6 + thh + 2, - size, namew, 20, subcol); - - /* 右侧 ⋯ 操作钮(三个点) */ - int ox = r_content.x + r_content.w - APP_OP_W / 2 - 1; - int oy = ry + APP_ROW_H / 2 - 8; - for (int d = 0; d < 3; d++) - meow_ui_fill_rect(ox, oy + d * 6, 4, 4, hilite ? 0xFFFFFF : th->onSurfaceVariant); - - /* 分隔线 */ - int sy = ry + APP_ROW_H - 1; - if (sy >= r_content.y && sy < r_content.y + r_content.h) - meow_ui_fill_rect(r_content.x, sy, r_content.w, 1, th->outlineVariant); - } + (void)ctx; + if (meow_ui_activity_active() != ACT_BROWSER) return; /* 非本页 active 不画(防跨页泄漏) */ + if (g_err == 0 && g_n != 0) return; + if (g_err == FSX_ERR_NOENT) + app_draw_empty(r_list, "NO STORAGE CARD", "insert a TF card / mount sd_0"); + else if (g_err == FSX_ERR_EACCES) + app_draw_empty(r_list, "ACCESS DENIED", "cannot read this folder"); + else if (g_err) + app_draw_empty(r_list, "CANNOT OPEN", "unexpected error"); + else + app_draw_empty(r_list, "EMPTY FOLDER", g_cur); +} +/* ---- 底部信息行绘制物(非控件):项数/路径/截断提示 ---- */ +static void browser_draw_footer(void *ctx) +{ + (void)ctx; + if (meow_ui_activity_active() != ACT_BROWSER) return; /* 非本页 active 不画(防跨页泄漏) */ + if (g_err != 0) return; char foot[96]; - if (g_n >= FSX_MAX_ENTRIES) + if (g_n == 0) + snprintf(foot, sizeof foot, "0 items"); + else if (g_n >= FSX_MAX_ENTRIES) snprintf(foot, sizeof foot, "%d items (showing first %d)", g_n, FSX_MAX_ENTRIES); else snprintf(foot, sizeof foot, "%d items %s", g_n, g_cur); app_draw_footer(r_content, foot); } -/* 列表 hit_cb:落点是否在内容区(行判定在 on_tap 里做)。 */ -static int browser_hit(meow_widget *w, int x, int y, void *ctx) -{ - (void)w; (void)ctx; - return x >= r_content.x && x < r_content.x + r_content.w && - y >= r_content.y && y < r_content.y + r_content.h; -} - /* ---- 导航 ---- */ static void browser_update_title(void) @@ -192,12 +138,15 @@ void browser_reload(void) int r = fsx_scan_dir(g_cur, g_entries, FSX_MAX_ENTRIES, &g_n); g_err = (r < 0) ? r : 0; if (g_err != 0) g_n = 0; - g_scroll = 0; - g_sel = -1; + if (g_list) { + meow_listview_set_count(g_list, g_n); /* 行数动态更新(§5.5) */ + meow_listview_set_scroll(g_list, 0); + meow_listview_set_sel(g_list, -1); + } browser_update_title(); const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); - meow_ui_paint_full(); + meow_ui_paint_full(); /* 重画全注册绘制物(行/空态/页脚/topbar) */ } void browser_enter_dir(const char *path) @@ -371,10 +320,9 @@ int browser_on_tap(int x, int y) } if (g_err != 0 || g_n == 0) return 0; if (meow_widget_hit(g_list, x, y)) { - int op = (x >= r_content.x + r_content.w - APP_OP_W); - int row = (y - r_content.y + g_scroll) / APP_ROW_H; - if (row < 0 || row >= g_n) { browser_drag_reset(); return 1; } - g_sel = row; + int row = meow_listview_hit(g_list, x, y); /* 行号(含滚动换算;禁用行 -1) */ + if (row < 0) { browser_drag_reset(); return 1; } /* 命中列表但无有效行(空隙) */ + int op = meow_listview_hit_op(g_list, x, y); /* 行右 1/3 = 操作区 */ if (op) browser_open_sheet(row); else { fsx_entry *e = &g_entries[row]; @@ -393,14 +341,15 @@ void browser_touch_down(int x, int y) browser_drag_reset(); if (g_err != 0 || g_n == 0) return; if (meow_widget_hit(g_list, x, y)) { - int row = (y - r_content.y + g_scroll) / APP_ROW_H; - if (row >= 0 && row < g_n) { + int row = meow_listview_hit(g_list, x, y); + if (row >= 0) { g_press_row = row; g_press_ms = meow_ui_time_ms(); + meow_listview_set_sel(g_list, row); /* 按下高亮(§5.8.3 selected) */ } g_dragging = 1; g_drag_last_y = y; - meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h); + meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } @@ -412,19 +361,18 @@ void browser_touch_move(int x, int y) int d = y - g_drag_last_y; if (d < 0) d = -d; if (d > 0) g_press_delta += d; - if (g_press_delta > 10) g_press_row = -1; + if (g_press_delta > 10) { + g_press_row = -1; + meow_listview_set_sel(g_list, -1); /* 拖动取消按下高亮 */ + } } int dy = y - g_drag_last_y; g_drag_last_y = y; if (dy == 0) return; - int ns = g_scroll - dy; - int ms = fsx_max_scroll(g_n, APP_ROW_H, browser_vp_h()); - if (ns < 0) ns = 0; - if (ns > ms) ns = ms; - if (ns != g_scroll) { - g_scroll = ns; - meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h); - } + int before = meow_listview_scroll(g_list); + meow_listview_scroll_by(g_list, -dy); /* set_scroll 内部 clamp 到 [0,max] */ + if (meow_listview_scroll(g_list) != before) + meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } void browser_swipe(int dir, int dist) @@ -432,14 +380,10 @@ void browser_swipe(int dir, int dist) if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; if (g_err != 0 || g_n == 0) return; int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 上滑→显示更低内容 */ - int ns = g_scroll + del; - int ms = fsx_max_scroll(g_n, APP_ROW_H, browser_vp_h()); - if (ns < 0) ns = 0; - if (ns > ms) ns = ms; - if (ns != g_scroll) { - g_scroll = ns; - meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h); - } + int before = meow_listview_scroll(g_list); + meow_listview_scroll_by(g_list, del); + if (meow_listview_scroll(g_list) != before) + meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } /* on_tick:长按(≥500ms 未拖动)→ 打开该行 BottomSheet。 */ @@ -460,14 +404,22 @@ static void browser_page_create(meow_view *v, void *ctx) (void)v; ((browser_ctrl *)ctx)->view = v; r_content = app_content_rect(); + r_list = AR(r_content.x, r_content.y, r_content.w, r_content.h - APP_FOOTER_H); + if (r_list.h < 0) r_list.h = 0; strncpy(g_cur, FSX_SD_ROOT, sizeof g_cur - 1); g_cur[sizeof g_cur - 1] = '\0'; g_tb = app_topbar_create("Files", 1, "NEW"); - g_list = meow_widget_custom_create(r_content.x, r_content.y, r_content.w, r_content.h, - browser_render, browser_hit, NULL); - g_sel = -1; + /* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */ + g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, + browser_src2, NULL, MEOW_LIST_SEL_SINGLE); + meow_listview_set_item_h(g_list, APP_ROW_H); + /* 空态/页脚注册为非控件绘制物(列表视口不含页脚;invalidate/paint_full 自动重画) */ + g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h, + browser_draw_empty, NULL); + g_footer_id = meow_draw_custom_register(1, r_content.x, r_content.y + r_content.h - APP_FOOTER_H, + r_content.w, APP_FOOTER_H, browser_draw_footer, NULL); g_press_row = -1; - browser_reload(); + browser_reload(); /* 扫首个目录(count/scroll/sel 由 reload 写入 listview) */ } static void browser_page_enter(meow_view *v, void *ctx) @@ -488,6 +440,8 @@ static void browser_page_leave(meow_view *v, void *ctx) static void browser_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; + if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; } + if (g_footer_id) { meow_draw_unregister(g_footer_id); g_footer_id = 0; } if (g_list) { meow_widget_free(g_list); g_list = NULL; } if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; } } diff --git a/patch_v2/hello_app/hello b/patch_v2/hello_app/hello index 22181ab..659611f 100755 Binary files a/patch_v2/hello_app/hello and b/patch_v2/hello_app/hello differ diff --git a/patch_v2/sdk/meow_ui.h b/patch_v2/sdk/meow_ui.h index c1df222..b150aef 100644 --- a/patch_v2/sdk/meow_ui.h +++ b/patch_v2/sdk/meow_ui.h @@ -567,6 +567,78 @@ typedef void (*meow_list_tap_fn)(int idx, void *ctx); void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx); /* 命中行→回调 */ int meow_list_tap_at(meow_widget *w, int x, int y); /* 命中行:置选中+回调,返回 idx/-1 */ +/* ============================================================ */ +/* §5.5 设计模板 A② 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作 + 选中 + * ------------------------------------------------------------ + * 在既有「label-only」列表(meow_list_create/lazycolumn,行为不变)之上做**结构化扩展**: + * 行数据源回调逐行给出 meow_list_item(图标 / 标题 / 副标题 / 右侧操作 / 可选中),容器负责 + * 绘制/滚动/选中/命中;支持单选/多选/无选择。向后兼容:既有 meow_list_* 全部保留不动。 + * 颜色全部取 meow_theme_get() 语义色(日/夜/style 自动),不硬编码。 + * 命中:meow_listview_hit 返回行号(含滚动换算 + 禁用行不响应);行右 1/3 为右侧操作区 + * (meow_listview_hit_op 另判)。抬手用最新坐标(goodix 免疫,参照既有 W_LIST 习惯)。 */ +/* ============================================================ */ + +/* 选中模式:无(纯入口列表)/ 单选 / 多选。 */ +typedef enum { MEOW_LIST_SEL_NONE = 0, MEOW_LIST_SEL_SINGLE, MEOW_LIST_SEL_MULTI } meow_list_sel; + +/* 行图标:meow_img*(MEOW_LIST_ICON_IMG)或内置符号(FOLDER/FILE,无图 App 用)。 */ +typedef enum { + MEOW_LIST_ICON_NONE = 0, /* 无图标 */ + MEOW_LIST_ICON_IMG, /* 用 item->icon(meow_img*) */ + MEOW_LIST_ICON_FOLDER, /* 内置目录符号(填充矩形+页签) */ + MEOW_LIST_ICON_FILE, /* 内置文件符号(空心矩形+折角) */ +} meow_list_icon_kind; + +/* 行右侧操作类型:无 / 箭头 / 开关 / 复选 / 详情钮(⋯)。 */ +typedef enum { + MEOW_LIST_RIGHT_NONE = 0, /* 无右侧操作 */ + MEOW_LIST_RIGHT_ARROW, /* 箭头 › */ + MEOW_LIST_RIGHT_SWITCH, /* 开关(on/off = item->on) */ + MEOW_LIST_RIGHT_CHECK, /* 复选标记 */ + MEOW_LIST_RIGHT_MORE, /* ⋯ 详情钮 */ +} meow_list_right; + +/* 单行数据(回调填充到栈上,容器不长期持有)。title 必填;subtitle 可空。 */ +typedef struct meow_list_item { + const meow_img *icon; /* 可选(icon_kind==MEOW_LIST_ICON_IMG 时用) */ + int icon_kind; /* MEOW_LIST_ICON_*(NONE=无图标) */ + const char *title; /* 主文本(必填,NULL 视为空行) */ + const char *subtitle; /* 副文本,可空 */ + int right; /* MEOW_LIST_RIGHT_* 右侧操作类型 */ + int on; /* right==SWITCH 时开关态(0/1) */ + int enabled; /* 1=可选中/可交互;0=变淡不响应命中 */ +} meow_list_item; + +/* 行数据源回调:idx 有效时填充 out 并返回 1;越界返回 0。每行轻量填充(不做 IO,§5.5)。 */ +typedef int (*meow_list2_source)(int idx, meow_list_item *out, void *ctx); + +/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。行高默认 56(可用 set_item_h 改)。 */ +meow_widget *meow_listview_create(int x, int y, int w, int h, int count, + meow_list2_source src, void *ctx, meow_list_sel mode); +/* 行高(固定;越界/<=0 忽略)。 */ +int meow_listview_set_item_h(meow_widget *lv, int item); +/* 行数(搜索过滤/懒加载/目录切换时动态更新):截断 sel 到新范围 + clamp scroll。 */ +void meow_listview_set_count(meow_widget *lv, int count); +/* 滚动:set 越界 clamp 到 [0, max_scroll];by 相对滚动(App on_swipe/touch_move 用)。 */ +void meow_listview_set_scroll(meow_widget *lv, int offset); +void meow_listview_scroll_by(meow_widget *lv, int dy); +int meow_listview_scroll(meow_widget *lv); +int meow_listview_max_scroll(meow_widget *lv); /* max(0, count*item_h - viewport_h) */ +/* 选中集(mode 语义): + * NONE :忽略;SINGLE:set_sel(idx) 设/清(idx<0 清除);MULTI:set_sel(idx) 翻转 idx。 + * get_sel 把当前选中下标写入 out[0..max),返回写入数;sel_count 返回选中总数。 */ +void meow_listview_set_sel(meow_widget *lv, int idx); +int meow_listview_get_sel(meow_widget *lv, int *out, int max); +int meow_listview_sel_count(meow_widget *lv); +/* 命中:返回行号(含滚动换算;禁用行/越界返回 -1);行右 1/3 = 右侧操作区(另判)。 */ +int meow_listview_hit(meow_widget *lv, int x, int y); +int meow_listview_hit_op(meow_widget *lv, int x, int y); /* 1=点在行右 1/3 操作区 */ +/* 行点击回调(idx=行号;op:0=行体,1=右侧操作区)。 */ +typedef void (*meow_listview_tap_fn)(int idx, int op, void *ctx); +void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ctx); +/* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */ +int meow_listview_tap_at(meow_widget *lv, int x, int y); + /* ============================================================ */ /* §5.8.6 模态(App 层,非系统叠加):Dialog / BottomSheet * 复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板(与 launcher s_dd_modal 同构),控件化。 diff --git a/patch_v2/sdk/meow_ui_widget.c b/patch_v2/sdk/meow_ui_widget.c index 21b428b..b3aa381 100644 --- a/patch_v2/sdk/meow_ui_widget.c +++ b/patch_v2/sdk/meow_ui_widget.c @@ -14,7 +14,8 @@ enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3, W_CUSTOM = 4, W_LAYOUT = 5, W_SPACER = 6, W_TEXTFIELD = 7, W_CARD = 8, W_DIVIDER = 9, W_BADGE = 10, W_CHECKBOX = 11, W_RADIO = 12, W_PROGRESS = 13, - W_TOPAPPBAR = 14, W_NAVBAR = 15, W_TABBAR = 16, W_FAB = 17 }; + W_TOPAPPBAR = 14, W_NAVBAR = 15, W_TABBAR = 16, W_FAB = 17, + W_LISTVIEW = 18 }; /* §5.5 结构化列表(行=图标+标题+副标题+右侧操作+选中) */ /* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */ #define MEOW_LAYOUT_MAX_CHILD 32 @@ -81,6 +82,14 @@ struct meow_widget { /* M2.71-bug8:W_LIST 行点击回调(可选)。 */ meow_list_tap_fn tap_cb; /* 命中行回调(可 NULL) */ void *tap_ctx; /* 回调 ctx */ + + /* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中(calloc 归零,安全) */ + meow_list2_source src2; /* 行数据源(meow_list_item) */ + int sel_mode; /* MEOW_LIST_SEL_* */ + uint32_t *sel_bits; /* 多选位图(sel_bits_n 个字;SINGLE 不分配) */ + int sel_bits_n; /* 位图字数 */ + meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */ + void *lv_tap_ctx; /* 回调 ctx */ }; /* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)---- @@ -328,6 +337,7 @@ void meow_widget_free(meow_widget *w) if (!w) return; meow_draw_widget_unregister(w); /* §6.2:绘制物注册表自动注销(stale id 失效勿用) */ live_btn_unregister(w); + if (w->sel_bits) { free(w->sel_bits); w->sel_bits = NULL; } /* W_LISTVIEW 多选位图 */ free(w); } @@ -881,6 +891,271 @@ int meow_list_tap_at(meow_widget *w, int x, int y) return idx; } +/* ============================================================ */ +/* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中 + * ------------------------------------------------------------ + * 行数据源(meow_list2_source)逐行填充 meow_list_item;容器只画可见行(虚拟化)+ 视口裁剪 + * (M2.73 纪律:只画视口内行/分隔线/文本,无标题栏/底部泄漏);局部 dirty;行高固定;首/末行 + * 钳制。颜色全部取 meow_theme_get() 语义色(日/夜/style 自动),禁止硬编码。 */ +/* ============================================================ */ +#define MEOW_LISTV_ICON 24 /* 图标盒尺寸(px) */ +#define MEOW_LISTV_PAD 16 /* 行内左右 padding(px) */ +#define MEOW_LISTV_SUB_GAP 2 /* 标题与副标题间隙(px) */ + +/* meow_ui_img_scaled 弱引用:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支用它绘制图片图标。 + * 弱引用让「只用内置符号图标、不解码图片」的 App 不被牵连进 meow_ui_img.o/stb_image.o + * (静态库惰性链接:无强引用则不拉入 stb 解码器,避免所有 App 无谓膨胀 ~77KB)。 + * 宿主/设备需图片图标的 App 经 meow_ui_img_load_* 强引用 meow_ui_img.o → 此处自动解析。 */ +extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attribute__((weak)); + +static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; } + +/* 文本截断:超出 maxw 加 "..."(复用 browser_text_trunc 的逐字符宽度累加)。 */ +static void listv_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) +{ + if (!s) return; + int full = meow_ui_text_width(s, size); + if (full <= maxw) { meow_ui_text(x, y, s, size, col); return; } + const char *dots = "..."; + int dw = meow_ui_text_width(dots, size); + char tmp[128]; + int i; + for (i = 0; s[i] && i < (int)sizeof tmp - 4; i++) { + tmp[i] = s[i]; + tmp[i + 1] = '\0'; + if (meow_ui_text_width(tmp, size) > maxw - dw) break; + } + memcpy(tmp + i, dots, 4); + meow_ui_text(x, y, tmp, size, col); +} + +/* 行图标绘制(内置符号 / meow_img)。col=图标前景(选中/未选中由调用方定)。 */ +static void listv_draw_icon(const meow_theme *th, int x, int y, int kind, + const meow_img *img, uint32_t col) +{ + (void)th; + int s = MEOW_LISTV_ICON; + if (kind == MEOW_LIST_ICON_IMG && img && meow_ui_img_scaled) { /* 弱符号非空才画(未链 stb 则跳过) */ + meow_ui_img_scaled(x, y, s, s, (meow_img *)img); /* img 为 opaque 句柄,const 丢弃 */ + return; + } + if (kind == MEOW_LIST_ICON_FOLDER) { + meow_ui_fill_rect(x, y + 6, s, s - 6, col); /* 主体 */ + meow_ui_fill_rect(x, y + 2, 12, 6, col); /* 页签 */ + } else if (kind == MEOW_LIST_ICON_FILE) { + meow_ui_draw_rect(x + 4, y + 2, s - 8, s - 4, col);/* 页体 */ + meow_ui_fill_rect(x + 4, y + 2, 7, 4, col); /* 文件名横条 */ + } +} + +/* 行右侧操作区绘制(箭头/开关/复选/⋯)。x=动作槽左缘;col=前景。 */ +static void listv_draw_right(const meow_theme *th, int x, int y, int row_h, + int right, int on, uint32_t col) +{ + switch (right) { + case MEOW_LIST_RIGHT_ARROW: { + int cx = x + 4, cy = y + row_h / 2; + meow_gfx_line(cx - 4, cy - 6, cx + 4, cy, 2, col); + meow_gfx_line(cx + 4, cy, cx - 4, cy + 6, 2, col); + break; + } + case MEOW_LIST_RIGHT_SWITCH: { + int sw = 36, sh = 20; + int sx = x, sy = y + (row_h - sh) / 2; + meow_gfx_fill_round(sx, sy, sw, sh, sh / 2, on ? th->primary : th->onSurfaceVariant); + int kr = sh / 2 - 3; + int kx = on ? sx + sw - kr * 2 - 3 : sx + 3; + meow_gfx_fill_round(kx, sy + 3, kr * 2, kr * 2, kr, on ? th->onPrimary : th->surface); + break; + } + case MEOW_LIST_RIGHT_CHECK: { + int cx = x + 2, cy = y + row_h / 2; + meow_gfx_line(cx - 6, cy + 2, cx - 1, cy + 7, 2, col); + meow_gfx_line(cx - 1, cy + 7, cx + 7, cy - 6, 2, col); + break; + } + case MEOW_LIST_RIGHT_MORE: { + int cx = x; + int cy0 = y + row_h / 2 - 8; + for (int d = 0; d < 3; d++) + meow_ui_fill_rect(cx, cy0 + d * 6, 4, 4, col); + break; + } + default: break; + } +} + +/* 多选位图扩容(MULTI 且 idx 超出当前容量时;SINGLE 不分配)。 */ +static void listv_bitmap_grow(meow_widget *w, int idx) +{ + int need = (idx + 1 + 31) / 32; + if (need <= w->sel_bits_n) return; + uint32_t *nb = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); + if (!nb) return; + if (w->sel_bits && w->sel_bits_n) + memcpy(nb, w->sel_bits, (size_t)w->sel_bits_n * sizeof(uint32_t)); + free(w->sel_bits); + w->sel_bits = nb; + w->sel_bits_n = need; +} + +meow_widget *meow_listview_create(int x, int y, int w, int h, int count, + meow_list2_source src, void *ctx, meow_list_sel mode) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_LISTVIEW; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx; + t->sel = -1; t->item_h = 56; t->scroll = 0; + t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE; + if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) { + t->sel_bits_n = (t->count + 31) / 32; + t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t)); + } + meow_draw_widget_register(t); + return t; +} + +int meow_listview_set_item_h(meow_widget *w, int item) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (item > 0) { w->item_h = item; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } + return w->item_h; +} +void meow_listview_set_count(meow_widget *w, int count) +{ + if (!w || w->type != W_LISTVIEW) return; + if (count < 0) count = 0; + if (count == w->count) return; + w->count = count; + if (w->sel >= w->count) w->sel = -1; + if (w->sel_mode == MEOW_LIST_SEL_MULTI) { /* 重分配多选位图 */ + free(w->sel_bits); w->sel_bits = NULL; w->sel_bits_n = 0; + int need = (count + 31) / 32; + if (need > 0) { + w->sel_bits_n = need; + w->sel_bits = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); + } + } + int ms = meow_listview_max_scroll(w); /* 行数减少时 clamp scroll */ + if (w->scroll > ms) w->scroll = ms; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} + +int meow_listview_max_scroll(meow_widget *w) +{ + if (!w || w->type != W_LISTVIEW) return 0; + int ms = w->count * listv_item_h(w) - w->h; + return ms < 0 ? 0 : ms; +} + +void meow_listview_set_scroll(meow_widget *w, int offset) +{ + if (!w || w->type != W_LISTVIEW) return; + int ms = meow_listview_max_scroll(w); + if (offset < 0) offset = 0; + if (offset > ms) offset = ms; + if (offset == w->scroll) return; + w->scroll = offset; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +void meow_listview_scroll_by(meow_widget *w, int dy) { if (w) meow_listview_set_scroll(w, w->scroll + dy); } +int meow_listview_scroll(meow_widget *w) { return (w && w->type == W_LISTVIEW) ? w->scroll : 0; } + +void meow_listview_set_sel(meow_widget *w, int idx) +{ + if (!w || w->type != W_LISTVIEW) return; + if (w->sel_mode == MEOW_LIST_SEL_NONE) return; + if (w->count <= 0) { w->sel = -1; return; } + if (idx >= w->count) idx = w->count - 1; /* 越界 clamp(§5.5 首/末行钳制) */ + if (w->sel_mode == MEOW_LIST_SEL_SINGLE) { + if (idx == w->sel) return; + w->sel = idx; /* idx<0 = 清除 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } else { /* MULTI:翻转 idx */ + if (idx < 0) { + if (w->sel_bits) memset(w->sel_bits, 0, (size_t)w->sel_bits_n * sizeof(uint32_t)); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return; + } + listv_bitmap_grow(w, idx); + if (!w->sel_bits) return; + w->sel_bits[idx >> 5] ^= (1u << (idx & 31)); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } +} +int meow_listview_get_sel(meow_widget *w, int *out, int max) +{ + if (!w || w->type != W_LISTVIEW) return 0; + if (!out || max <= 0) return 0; + int n = 0; + if (w->sel_mode == MEOW_LIST_SEL_MULTI) { + for (int i = 0; i < w->count && n < max; i++) + if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) out[n++] = i; + } else if (w->sel >= 0 && n < max) { + out[n++] = w->sel; + } + return n; +} +int meow_listview_sel_count(meow_widget *w) +{ + if (!w || w->type != W_LISTVIEW) return 0; + if (w->sel_mode == MEOW_LIST_SEL_MULTI) { + int n = 0; + for (int i = 0; i < w->count; i++) + if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++; + return n; + } + return (w->sel >= 0) ? 1 : 0; +} + +/* 命中:返回行号(含滚动换算;禁用行/越界 -1)。抬手用最新坐标(goodix 免疫,调用方传抬手落点)。 */ +int meow_listview_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int item = listv_item_h(w); + int idx = (y - w->y + w->scroll) / item; + if (idx < 0 || idx >= w->count) return -1; + if (w->src2) { /* 禁用行不响应(§5.5) */ + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2(idx, &it, w->ctx)) return -1; + if (!it.enabled) return -1; + } + return idx; +} +int meow_listview_hit_op(meow_widget *w, int x, int y) +{ + int idx = meow_listview_hit(w, x, y); + if (idx < 0) return 0; + int opx = w->x + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5) */ + if (x < opx) return 0; + if (w->src2) { + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2(idx, &it, w->ctx)) return 0; + if (it.right == MEOW_LIST_RIGHT_NONE) return 0; + } + return 1; +} +void meow_listview_set_on_tap(meow_widget *w, meow_listview_tap_fn cb, void *ctx) +{ + if (!w || w->type != W_LISTVIEW) return; + w->lv_tap_cb = cb; w->lv_tap_ctx = ctx; +} +int meow_listview_tap_at(meow_widget *w, int x, int y) +{ + int idx = meow_listview_hit(w, x, y); + if (idx < 0) return -1; + int op = meow_listview_hit_op(w, x, y); + meow_listview_set_sel(w, idx); + if (w->lv_tap_cb) w->lv_tap_cb(idx, op, w->lv_tap_ctx); + return idx; +} + static int hit(meow_widget *w, int x, int y) { if (!w) return 0; @@ -1071,6 +1346,84 @@ void meow_widget_draw(meow_widget *w) if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; } + case W_LISTVIEW: { + /* §5.5 结构化列表(纵向 LazyColumn):只画可见行(虚拟化)+ 视口裁剪(M2.73 纪律)。 + * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer / + * onPrimaryContainer(§5.8.3);禁用行变淡不响应(命中由 hit 层保证)。全语义色。 */ + int item = listv_item_h(w); + int vp = w->h; /* 纵向 */ + int off = w->scroll; + int maxs = w->count * item - vp; + if (maxs < 0) maxs = 0; + if (off < 0) off = 0; + if (off > maxs) off = maxs; + int first = (item > 0) ? off / item : 0; + int last = (item > 0) ? (off + vp) / item + 1 : 0; + if (first < 0) first = 0; + if (last > w->count) last = w->count; + int vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h; + int thh = meow_ui_text_height(th->font_body); + int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON; + int text_x = vx0 + pad + isz + pad; + int op_start = vx0 + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5 命中) */ + int text_maxw = op_start - pad - text_x; /* 标题/副标题可用宽(不压操作区) */ + if (text_maxw < 8) text_maxw = 8; + for (int i = first; i < last; i++) { + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2 || !w->src2(i, &it, w->ctx)) continue; + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + if (ry + item <= vy0) continue; + int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ + int sel = (i == w->sel); + uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; + uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; + if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); + + /* 图标(左) */ + uint32_t iccol = sel ? th->onPrimaryContainer + : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary + : th->onSurfaceVariant); + int icy = ry + (item - isz) / 2; + if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ + listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + + /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ + int ty; + if (it.subtitle && it.subtitle[0]) { + ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + int sy = ty + thh + MEOW_LISTV_SUB_GAP; + if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); + } else { + ty = ry + (item - thh) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + } + + /* 右侧操作区 */ + if (it.right != MEOW_LIST_RIGHT_NONE) { + int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ + if (ax >= op_start) + listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); + } + + /* 分隔线(仅落在可见带内才画) */ + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + + /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ + if (!it.enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + } + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } case W_CUSTOM: /* SDK 不替自定义画布画背景/状态层;App 在 render_cb 里自绘(th 供取色), * 可读 meow_widget_state(状态位)/ meow_widget_opts_get(通用修饰)自行叠。 */ diff --git a/patch_v2/sdk/test_ime_fix.c b/patch_v2/sdk/test_ime_fix.c index 74f5891..dd9593e 100644 --- a/patch_v2/sdk/test_ime_fix.c +++ b/patch_v2/sdk/test_ime_fix.c @@ -8,8 +8,9 @@ * gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ - * meow_ui_draw.c meow_ui_view.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread + * meow_ui_draw.c meow_ui_view.c meow_ui_img.c stb_image.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm) */ #include "meow_ui.h" #include "meow_ui_internal.h" diff --git a/patch_v2/sdk/test_meow_ui_widgets.c b/patch_v2/sdk/test_meow_ui_widgets.c index 18d4c59..e111176 100644 --- a/patch_v2/sdk/test_meow_ui_widgets.c +++ b/patch_v2/sdk/test_meow_ui_widgets.c @@ -5,16 +5,74 @@ * gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ - * meow_ui_draw.c meow_ui_view.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread + * meow_ui_draw.c meow_ui_view.c meow_ui_img.c stb_image.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕) + * + * 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm。 + * 结构化列表断言用 back_buf 像素(region_has/px_near),画完即读,非目检。 */ #include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ #include #include #include #include +/* ---- back_buf 像素读取(断言真实渲染:画完读像素,不依赖目检) ---- */ +static void rd_px(int x, int y, int *r, int *g, int *b) +{ + unsigned char *bb = meow_ui_back_buf(); + int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp(); + if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; } + if (bpp == 16) { + uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2); + *r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2; + *g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4; + *b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2); + } else { + uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4); + *r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF; + } +} +static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; } +static int px_near(int x, int y, int r, int g, int b) +{ + int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb); + return near2(rr, r) && near2(gg, g) && near2(bb, b); +} +/* 区域内含「接近 target 色」的像素数(>0 即该色被真实绘出)。 */ +static int region_has(int x, int y, int w, int h, int r, int g, int b) +{ + int cnt = 0; + for (int dy = 0; dy < h; dy++) + for (int dx = 0; dx < w; dx++) + if (px_near(x + dx, y + dy, r, g, b)) cnt++; + return cnt; +} +#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0) + +/* 结构化列表(W_LISTVIEW)测试数据源 */ +typedef struct { const char *title; const char *sub; int ik; int right; int enabled; } lvrow_t; +static lvrow_t g_lvrows[4] = { + { "Row A", "sub A", MEOW_LIST_ICON_FOLDER, MEOW_LIST_RIGHT_MORE, 1 }, + { "Row B", "sub B", MEOW_LIST_ICON_FILE, MEOW_LIST_RIGHT_NONE, 1 }, + { "Row C", "sub C", MEOW_LIST_ICON_NONE, MEOW_LIST_RIGHT_ARROW, 0 }, /* 禁用 */ + { "Row D", "sub D", MEOW_LIST_ICON_FOLDER, MEOW_LIST_RIGHT_CHECK, 1 }, +}; +static int lv_src(int idx, meow_list_item *out, void *ctx) +{ + (void)ctx; + if (idx < 0 || idx >= 4) return 0; + out->icon_kind = g_lvrows[idx].ik; + out->title = g_lvrows[idx].title; + out->subtitle = g_lvrows[idx].sub; + out->right = g_lvrows[idx].right; + out->on = 1; + out->enabled = g_lvrows[idx].enabled; + return 1; +} + /* 测试数据源(列表容器) */ static int list_src(int idx, char *label, int len, void *ctx) { @@ -158,6 +216,75 @@ int main(void) assert(meow_list_hit_index(lr, 40, 660) == ((40 - 20 + 200) / 60)); printf("PASS lazy list: scroll clamp/virtualize/hit_index/tap_at/select (col+row)\n"); + /* ---------- 结构化列表(W_LISTVIEW):行渲染像素 + 命中 + 滚动裁剪 ---------- + * lv 位于 (20,240,320,112)item_h=56 → 行0 y240..296、行1 296..352。 + * 断言全部「画完读 back_buf 像素」(region_has/px_near),非目检。 */ + meow_widget *lv = W(meow_listview_create(20, 240, 320, 112, 4, lv_src, NULL, MEOW_LIST_SEL_SINGLE)); + assert(meow_listview_set_item_h(lv, 56) == 56); + const meow_theme *lth = meow_theme_get(); + int lvx = 20, lvy = 240, lvw = 320, lvh = 112, itemh = 56; + int textx = lvx + 16 + 24 + 16; /* 文本起 x = 76 */ + int op_start = lvx + (lvw * 2 / 3); /* 行右 1/3 操作区起 = 20+213=233 */ + int sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b; + int ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b; + UNPK(lth->bg, sb_r, sb_g, sb_b); + UNPK(lth->onSurface, os_r, os_g, os_b); + UNPK(lth->primaryContainer, pc_r, pc_g, pc_b); + UNPK(lth->onSurfaceVariant, ospv_r, ospv_g, ospv_b); + UNPK(lth->primary, prim_r, prim_g, prim_b); + int thh = meow_ui_text_height(lth->font_body); /* 20(宿主 5x7) */ + + /* 行 0 的标题/副标题 y(含 subtitle 的两行居中):ty=240+(56-42)/2=247 */ + int r0_ty = lvy + (itemh - (2 * thh + 2)) / 2; + int r0_sy = r0_ty + thh + 2; + + /* 1) 行内容渲染:清底 + 只画列表,断言图标/标题/副标题/右侧 ⋯ 真实像素 */ + meow_ui_clear(lth->bg); + meow_widget_draw(lv); + assert(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 100); + assert(region_has(textx, r0_ty, 80, thh, os_r, os_g, os_b) > 2); + assert(region_has(textx, r0_sy, 80, thh, ospv_r, ospv_g, ospv_b) > 2); + assert(region_has(lvx + lvw - 16 - 24, lvy + (itemh - 20) / 2, 8, 20, ospv_r, ospv_g, ospv_b) > 20); + printf("PASS listview: row render (icon/title/subtitle/right-more) pixels\n"); + + /* 2) 选中态:行 1 → primaryContainer 高亮;行 0 无高亮(bg) */ + meow_listview_set_sel(lv, 1); + meow_ui_clear(lth->bg); + meow_widget_draw(lv); + assert(px_near(28, lvy + itemh + 48, pc_r, pc_g, pc_b)); /* 行1带底部空白 = primaryContainer */ + assert(px_near(28, lvy + 48, sb_r, sb_g, sb_b)); /* 行0带空白 = bg(未选中无高亮) */ + int selout[4]; + assert(meow_listview_get_sel(lv, selout, 4) == 1 && selout[0] == 1); + assert(meow_listview_sel_count(lv) == 1); + printf("PASS listview: selected row highlight + get_sel/sel_count\n"); + + /* 3) 命中:行号 + 右侧操作区分 + 禁用行不命中 */ + assert(meow_listview_hit(lv, 28, lvy + 20) == 0); /* 行0 体 */ + assert(meow_listview_hit_op(lv, 28, lvy + 20) == 0); /* 体非操作区 */ + assert(meow_listview_hit_op(lv, op_start + 10, lvy + 20) == 1); /* 行0 右1/3=操作区 */ + meow_listview_set_sel(lv, -1); + /* 滚动到 3、4 行行内命中(scroll=112 → 行2 y240、行3 y296) */ + meow_listview_set_scroll(lv, 112); + assert(meow_listview_scroll(lv) == 112); + assert(meow_listview_max_scroll(lv) == 4 * 56 - 112); + assert(meow_listview_hit(lv, 28, lvy + 28) == -1); /* 行2 禁用 → 不命中 */ + assert(meow_listview_hit(lv, 28, lvy + itemh + 28) == 3); /* 行3 命中 */ + printf("PASS listview: hit row/hit_op/disabled-not-hit\n"); + + /* 4) 滚动视口裁剪:滚到 112 后画,视口外(上缘/下缘)无泄漏(保持 bg) */ + meow_ui_clear(lth->bg); + meow_widget_draw(lv); + assert(px_near(21, lvy - 1, sb_r, sb_g, sb_b)); /* 视口上缘外 = bg(无上漏) */ + assert(px_near(21, lvy + lvh, sb_r, sb_g, sb_b)); /* 视口下缘外 = bg(无下漏) */ + /* 视口内 行3 标题真实渲染(行3 y=296,标题 y=303) */ + assert(region_has(textx, lvy + itemh + (itemh - (2 * thh + 2)) / 2, + 80, thh, os_r, os_g, os_b) > 2); + /* 行 2 禁用行「变淡」:draw 后其左 padding 像素应被 dim 覆盖(偏离纯 bg) */ + assert(!px_near(28, lvy + 28, sb_r, sb_g, sb_b)); + printf("PASS listview: scroll clip no-leak + disabled-row dim\n"); + meow_listview_set_scroll(lv, 0); + meow_listview_set_sel(lv, -1); + /* ---------- Dialog / BottomSheet 纯逻辑(create/set/add;show 阻塞故不调) ---------- */ meow_dialog *dlg = meow_dialog_create(MEOW_DLG_SELECT, "Pick one"); const char *items[] = { "Alpha", "Beta", "Gamma" };