From 7f36b6f069235c322c5ba461e6bdb8de6f5b78b3 Mon Sep 17 00:00:00 2001 From: MeowOS Date: Sun, 30 Aug 2026 22:33:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E6=8E=A7=E4=BB=B6=E5=BA=93?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E6=89=B9=20Material=20=E5=AD=90=E9=9B=86(age?= =?UTF-8?q?nt-152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增控件: Button五型(filled/tonal/outlined/text/elevated)/TextField(接IME: round外框idle-outline/focused-primary+占位+光标)/Card/Divider/Badge/Checkbox/Radio/ProgressBar(线圆, value=-1不定)/FAB/TopAppBar(标题+‹返回+动作)/NavigationBar(TabBar pageSwitcher)+TabBar(下划线)/LazyColumn/LazyRow(虚拟化+scroll clamp) - Dialog(B③同构阻塞模态)/BottomSheet(B④), meow_ui_dialog.c - meow_gfx 新增 fill_circle/line/ring_arc(无-lm, COS90象限查表) - 结构体尾缀(对外opaque/calloc归零), 旧字段与create签名零破坏; meow_widget 类型扩到W_TEXTFIELD..W_FAB(17种) - meow_ui.a=18c21da5 hello=5e31d919 launcher=4952261d(重编重链无回归); build_sdk 0warning; 宿主 test_meow_ui_widgets 全PASS+test_sb_collapse PASS+PPM目检正确 - 与 §5.8/§5.2-5.7 一致; 遗留真机观感/性能 --- build_sdk.sh | 2 +- meow_ui.h | 114 +++++++ meow_ui_dialog.c | 293 ++++++++++++++++++ meow_ui_gfx.c | 100 ++++++- meow_ui_internal.h | 7 + meow_ui_widget.c | 664 +++++++++++++++++++++++++++++++++++++++-- test_host_stub.c | 24 ++ test_meow_ui_widgets.c | 180 +++++++++++ 8 files changed, 1360 insertions(+), 24 deletions(-) create mode 100644 meow_ui_dialog.c create mode 100644 test_host_stub.c create mode 100644 test_meow_ui_widgets.c diff --git a/build_sdk.sh b/build_sdk.sh index b0fe874..cecf0c5 100755 --- a/build_sdk.sh +++ b/build_sdk.sh @@ -10,7 +10,7 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架 rm -f meow_ui.a *.o # -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移) -OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global stb_image" +OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog stb_image" CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd" for f in $OBJS; do echo "== cc $f ==" diff --git a/meow_ui.h b/meow_ui.h index 95b4821..a64dd49 100644 --- a/meow_ui.h +++ b/meow_ui.h @@ -403,6 +403,120 @@ void meow_layout_arrange(meow_widget *layout); /* 固定尺寸空白(也作撑余量;不可见,hit 恒 0)。 */ meow_widget *meow_layout_spacer(int w, int h); +/* ============================================================ */ +/* §5.8.6 Material 3 组件子集(第二批,基于第一批基座:Options + 状态机 + MD3 色板 + 布局) + * 每个组件 = 新增 meow_widget 类型或组合既有模板的便捷函数;复用主题语义色(th->primary / + * surfaceContainer / outline / ...)+ 风格参数(radius/radius_card/spacing/stroke_w)+ 通用 + * Options(meow_widget_set_opts 对它们同样生效)+ 状态机(enabled/selected/pressed/focused)。 + * 向后兼容:既有 create 签名一字不改;本批只在其上扩展。 */ +/* ============================================================ */ + +/* ---- Button 五型(meow_btn 扩展;既有 meow_btn_create = FILLED)---- */ +enum { + MEOW_BTN_FILLED = 0, /* primary 底 / onPrimary 文 */ + MEOW_BTN_TONAL = 1, /* secondaryContainer 底 / onSecondaryContainer 文 */ + MEOW_BTN_OUTLINED = 2, /* 透明底 / primary 文 / primary 描边 */ + MEOW_BTN_TEXT = 3, /* 透明底 / primary 文 / 无描边 */ + MEOW_BTN_ELEVATED = 4, /* surfaceContainerLow 底 / primary 文 */ +}; +meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *label, int variant); +int meow_btn_set_variant(meow_widget *w, int variant); /* 越界 clamp;0=成功 */ + +/* ---- TextField(单行文本输入,接 IME §5.1)---- */ +/* placeholder=提示文案;buf=App 持有的文本缓冲(组件不回拷,只绘制/编辑其上);maxlen=含 NUL 预算 */ +meow_widget *meow_textfield_create(int x, int y, int w, int h, const char *placeholder, + char *buf, int maxlen, meow_ime_mode mode); +void meow_textfield_set_placeholder(meow_widget *w, const char *placeholder); +/* 聚焦并打开**阻塞** IME(同 meow_ui_text_input 内层轮询);返回 1=确认 0=取消;buf 已回填 */ +int meow_textfield_activate(meow_widget *w); +const char *meow_textfield_text(meow_widget *w); + +/* ---- Card(圆角容器卡片;surfaceContainerLow 底 + 可选描边/状态层)---- */ +meow_widget *meow_card_create(int x, int y, int w, int h); +/* 内容子控件:绘制时递归绘到卡片内(卡片 hit 仍为矩形;子控件独立命中) */ +void meow_card_set_content(meow_widget *card, meow_widget *child); + +/* ---- Divider(分隔线;vertical=1 纵向)---- */ +meow_widget *meow_divider_create(int x, int y, int len, int vertical); + +/* ---- Badge(角标;count=0 红点 / >0 数字 / <0 隐藏;cx,cy=锚点=角标中心)---- */ +meow_widget *meow_badge_create(int cx, int cy); +void meow_badge_set(meow_widget *w, int count); + +/* ---- Checkbox / Radio(勾选态走 meow_widget_set_selected/meow_widget_selected)---- */ +meow_widget *meow_checkbox_create(int x, int y); +meow_widget *meow_radio_create(int x, int y); + +/* ---- ProgressBar(线性/圆形;value=0-100 确定性,-1=不定)---- */ +meow_widget *meow_progress_create(int x, int y, int w, int h, int value); /* 线性 */ +meow_widget *meow_progress_circular_create(int cx, int cy, int r, int value); /* 圆形 */ +void meow_progress_set(meow_widget *w, int value); /* -1=不定 */ + +/* ---- FloatingActionButton(悬浮按钮;圆角胶囊,无投影仅状态层近似)---- */ +meow_widget *meow_fab_create(int x, int y, int size, const char *label); + +/* ---- TopAppBar(顶部标题栏:标题 + 返回 + 动作)---- */ +meow_widget *meow_topappbar_create(int x, int y, int w, int h, const char *title); +void meow_topappbar_set_title(meow_widget *w, const char *title); +void meow_topappbar_set_back(meow_widget *w, int on); /* 1=显示返回箭头 */ +int meow_topappbar_add_action(meow_widget *w, const char *label); /* 返回动作下标 */ +#define MEOW_TOPBAR_BACK (-2) +int meow_topappbar_hit(meow_widget *w, int x, int y); /* MEOW_TOPBAR_BACK=返回 idx>=0=动作 -1=无 */ + +/* ---- NavigationBar(底部导航栏)---- */ +meow_widget *meow_navigationbar_create(int x, int y, int w, int h); +int meow_navigationbar_add_item(meow_widget *w, const char *label); +void meow_navigationbar_set_selected(meow_widget *w, int idx); +int meow_navigationbar_selected(meow_widget *w); +int meow_navigationbar_hit(meow_widget *w, int x, int y); /* 返回项下标/-1 */ + +/* ---- TabBar(顶部分页页签;swipe 联动 §5.2 由 App 在 on_swipe 驱动)---- */ +meow_widget *meow_tabbar_create(int x, int y, int w, int h); +int meow_tabbar_add_tab(meow_widget *w, const char *label); +void meow_tabbar_set_selected(meow_widget *w, int idx); +int meow_tabbar_hit(meow_widget *w, int x, int y); /* 返回页签下标/-1 */ + +/* ---- 列表容器(LazyColumn/LazyRow:数据源回调 + 可见区虚拟化;复用 meow_list_source) + * meow_lazycolumn_create=纵向(item_h=行高);meow_lazyrow_create=横向(item_w=列宽)。 + * 滚动由 App 在 on_swipe 里用 meow_list_scroll_by/set_scroll 驱动。 */ +meow_widget *meow_lazycolumn_create(int x, int y, int w, int h, int item_h, int count, + meow_list_source src, void *ctx); +meow_widget *meow_lazyrow_create(int x, int y, int w, int h, int item_w, int count, + meow_list_source src, void *ctx); +void meow_list_set_scroll(meow_widget *w, int offset); /* clamp 到 [0, max_scroll] */ +int meow_list_scroll(meow_widget *w); +int meow_list_max_scroll(meow_widget *w); /* max(0, count*item - viewport) */ +void meow_list_scroll_by(meow_widget *w, int dy); /* 相对滚动(App on_swipe 用) */ +int meow_list_set_item_h(meow_widget *w, int item); /* 改行高/列宽 */ +int meow_list_hit_index(meow_widget *w, int x, int y); /* 命中项下标(含滚动换算);-1=未命中项/越界 */ + +/* ============================================================ */ +/* §5.8.6 模态(App 层,非系统叠加):Dialog / BottomSheet + * 复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板(与 launcher s_dd_modal 同构),控件化。 + * 阻塞式(内层 meow_input_pop 轮询,与 meow_ui_text_input 同构);打开前排空残留触摸。 + * 关闭途径:点卡片内选项=确认、点卡片外=取消、左缘返回手势=取消。不压状态栏(y 钳制)。 */ +/* ============================================================ */ + +/* ---- Dialog(弹窗:确认/选择/输入/信息)---- */ +typedef enum { MEOW_DLG_CONFIRM, MEOW_DLG_SELECT, MEOW_DLG_INPUT, MEOW_DLG_INFO } meow_dlg_t; +typedef struct meow_dialog meow_dialog; +meow_dialog *meow_dialog_create(meow_dlg_t t, const char *title); +void meow_dialog_set_items(meow_dialog *d, const char *const *item, int n, int cur); /* SELECT 型 */ +void meow_dialog_set_ime(meow_dialog *d, meow_ime_ctx *ime); /* INPUT 型 */ +void meow_dialog_show(meow_dialog *d); /* 阻塞到确认/取消 */ +int meow_dialog_result(meow_dialog *d); /* SELECT=选项下标;CONFIRM/INFO 确认=1、取消=0 */ +void meow_dialog_close(meow_dialog *d); /* 程序化关闭(App 亦可驱动) */ +void meow_dialog_free(meow_dialog *d); +/* CONFIRM 型便捷:点「确定」返回 1,取消/遮罩=0 */ +int meow_dialog_confirm(meow_dialog *d); + +/* ---- BottomSheet(底部面板:标题 + 选项列表;open 阻塞返回选中 idx/-1)---- */ +typedef struct meow_sheet meow_sheet; +meow_sheet *meow_sheet_create(const char *title, int h); +int meow_sheet_add_item(meow_sheet *s, const char *label); /* 返回项下标 */ +int meow_sheet_open(meow_sheet *s); /* 阻塞;返回选中 idx / -1=取消 */ +void meow_sheet_free(meow_sheet *s); + /* ============================================================ */ /* §9 应用日志(统一日志 API,M2.51 最小可用版) */ /* ============================================================ */ diff --git a/meow_ui_dialog.c b/meow_ui_dialog.c new file mode 100644 index 0000000..3facc68 --- /dev/null +++ b/meow_ui_dialog.c @@ -0,0 +1,293 @@ +/* + * meow_ui_dialog.c — §5.8.6 模态控件化(App 层,非系统叠加):Dialog / BottomSheet。 + * + * 复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板(与 launcher s_dd_modal 同构):遮罩压暗 + + * 圆角卡片 + 标题 + 内容 + 操作;返回手势=取消;不压状态栏。阻塞式(内层 meow_input_pop + * 轮询,与 meow_ui_text_input 同构),打开前排空残留触摸(goodix 长尾免疫)。 + * 命中一律用抬手最新坐标;即画即显(打开一次性绘制,仅高亮变化才重绘,逐帧不重画静态内容)。 + * + * 属 App 层模态:不注册进系统顶条/通知中心渲染(系统级控件归属不动)。物理键在 B1 未下发 + * 给 App(on_key 无键),故本层关闭途径=触摸 + 左缘返回手势;音量/电源高亮导航为后续批次。 + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" +#include +#include +#include +#include + +#define MEOW_MODAL_MAX_ITEMS 32 +#define MEOW_MODAL_ROW 48 +#define MEOW_MODAL_BTN 44 + +/* ---- Dialog ---- */ +struct meow_dialog { + meow_dlg_t type; + char title[96]; + char items[MEOW_MODAL_MAX_ITEMS][64]; + int nitems; + int cur; /* SELECT 预选/高亮项 */ + int result; + int done; + meow_ime_ctx ime; /* INPUT 型(拷贝) */ + int has_ime; +}; + +/* 卡片几何:返回标题之后内容起点(行/按钮相对坐标)。 */ +typedef struct { int dx,dy,dw,dh; int row_y,rowh; int btn_y,btn_w,btn_h; } dlg_geom; + +static void dlg_calc(meow_dialog *d, const meow_theme *th, dlg_geom *g) +{ + int W = meow_ui_width(), H = meow_ui_height(); + int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0; + int pad = 24; + int tH = meow_ui_text_height(th->font_title); + int bH = meow_ui_text_height(th->font_body); + int select = (d->type == MEOW_DLG_SELECT); + int nrows = select ? d->nitems : 0; + int nmsg = (d->type == MEOW_DLG_INFO) ? (d->nitems > 0 ? d->nitems : 1) + : (select ? 0 : (d->nitems > 0 ? d->nitems : 1)); + int have_btns = !select; + g->dw = 348; if (g->dw > W - 40) g->dw = W - 40; + g->dx = (W - g->dw) / 2; + int content = nrows * MEOW_MODAL_ROW + nmsg * (bH + 8); + g->dh = pad + tH + 12 + content + pad + (have_btns ? 16 + MEOW_MODAL_BTN : 0); + int avail = H - sb - 20; + if (g->dh > avail) g->dh = avail; + g->dy = (H - g->dh) / 2; if (g->dy < sb) g->dy = sb; + g->row_y = g->dy + pad + tH + 12; + g->rowh = (MEOW_MODAL_ROW < 0) ? 40 : MEOW_MODAL_ROW; + g->btn_y = g->row_y + content + 16; + g->btn_h = MEOW_MODAL_BTN; + g->btn_w = (g->dw - 2 * pad - 12) / 2; +} + +static void dlg_paint(meow_dialog *d, const meow_theme *th) +{ + int W = meow_ui_width(), H = meow_ui_height(); + meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); /* 遮罩压暗 */ + dlg_geom g; + dlg_calc(d, th, &g); + int pad = 24; + meow_gfx_fill_round(g.dx, g.dy, g.dw, g.dh, th->radius_card, th->surfaceContainerHighest); + meow_ui_draw_rect_round(g.dx, g.dy, g.dw, g.dh, th->radius_card, th->outline); + meow_ui_text(g.dx + pad, g.dy + pad, d->title, th->font_title, th->onSurface); + + if (d->type == MEOW_DLG_SELECT) { + for (int i = 0; i < d->nitems; i++) { + int ry = g.row_y + i * g.rowh; + if (ry + g.rowh > g.dy + g.dh - 12) break; + if (i == d->cur) { + meow_ui_fill_rect(g.dx + 8, ry, g.dw - 16, g.rowh, th->primary); + meow_ui_text(g.dx + pad, ry + (g.rowh - meow_ui_text_height(th->font_body)) / 2, + d->items[i], th->font_body, 0xFFFFFF); + } else { + meow_ui_text(g.dx + pad, ry + (g.rowh - meow_ui_text_height(th->font_body)) / 2, + d->items[i], th->font_body, th->onSurface); + } + meow_ui_fill_rect(g.dx + pad, ry + g.rowh - 1, g.dw - 2 * pad, 1, th->outlineVariant); + } + } else { + /* INFO/CONFIRM:正文行;INPUT:文本预览 */ + int nmsg = (d->type == MEOW_DLG_INFO) ? (d->nitems > 0 ? d->nitems : 1) + : (d->nitems > 0 ? d->nitems : 1); + int y = g.row_y; + if (d->type == MEOW_DLG_INPUT) { + char buf[300]; + snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : "(tap OK to edit...)"); + meow_ui_text(g.dx + pad, y, buf, th->font_body, th->onSurfaceVariant); + } else { + for (int i = 0; i < nmsg; i++) { + if (i < d->nitems && d->items[i][0]) + meow_ui_text(g.dx + pad, y, d->items[i], th->font_body, th->onSurfaceVariant); + y += meow_ui_text_height(th->font_body) + 8; + } + } + /* 取消(左,text 型)/ 确定(右,filled) */ + int bx = g.dx + pad, cx = bx + g.btn_w + 12, by = g.btn_y; + meow_ui_draw_rect_round(bx, by, g.btn_w, g.btn_h, 10, th->primary); + meow_ui_text_center(bx + g.btn_w / 2, by + (g.btn_h - meow_ui_text_height(th->font_body)) / 2, + "CANCEL", th->font_body, th->primary); + meow_gfx_fill_round(cx, by, g.btn_w, g.btn_h, 10, th->primary); + meow_ui_text_center(cx + g.btn_w / 2, by + (g.btn_h - meow_ui_text_height(th->font_body)) / 2, + "OK", th->font_body, th->onPrimary); + } +} + +meow_dialog *meow_dialog_create(meow_dlg_t t, const char *title) +{ + meow_dialog *d = (meow_dialog *)calloc(1, sizeof *d); + if (!d) return NULL; + d->type = t; + if (title) { strncpy(d->title, title, sizeof d->title - 1); d->title[sizeof d->title - 1] = 0; } + d->cur = -1; d->result = 0; d->done = 0; + return d; +} +void meow_dialog_set_items(meow_dialog *d, const char *const *item, int n, int cur) +{ + if (!d) return; + d->nitems = 0; + if (item) for (int i = 0; i < n && i < MEOW_MODAL_MAX_ITEMS; i++) + if (item[i]) { strncpy(d->items[d->nitems], item[i], 63); d->items[d->nitems][63] = 0; d->nitems++; } + d->cur = cur; +} +void meow_dialog_set_ime(meow_dialog *d, meow_ime_ctx *ime) +{ + if (!d || !ime) return; + d->ime = *ime; + d->has_ime = 1; +} + +void meow_dialog_close(meow_dialog *d) +{ + if (d) d->done = 1; +} + +void meow_dialog_show(meow_dialog *d) +{ + if (!d) return; + const meow_theme *th = meow_theme_get(); + /* INPUT 型若未带 ime,用空 ctx 兜底 */ + if (d->type == MEOW_DLG_INPUT && !d->has_ime) { + memset(&d->ime, 0, sizeof d->ime); + d->ime.maxlen = 255; + } + meow_input_start(); + meow_input_drain(); /* 排空残留触摸(goodix 长尾) */ + d->done = 0; d->result = 0; + dlg_paint(d, th); + meow_ui_flush(); + + while (!d->done) { + meow_input_ev e; + while (meow_input_pop(&e)) { + if (e.type == MEOW_EV_TAP) { + int x = e.a, y = e.b; + dlg_geom g; + dlg_calc(d, th, &g); + int in_card = x >= g.dx && x < g.dx + g.dw && y >= g.dy && y < g.dy + g.dh; + if (!in_card) { d->done = 1; d->result = 0; break; } /* 遮罩外=取消 */ + if (d->type == MEOW_DLG_SELECT) { + int idx = (y - g.row_y) / g.rowh; + if (idx >= 0 && idx < d->nitems) { d->cur = idx; d->result = idx; d->done = 1; } + break; + } + /* 非选择:按钮命中 */ + int pad = 24, by = g.btn_y; + int bx = g.dx + pad, cx = bx + g.btn_w + 12; + if (y >= by && y < by + g.btn_h) { + if (x >= bx && x < bx + g.btn_w) { /* CANCEL */ + d->done = 1; d->result = 0; + } else if (x >= cx && x < cx + g.btn_w) { /* OK */ + if (d->type == MEOW_DLG_INPUT) { + d->result = meow_ui_text_input(&d->ime); /* 阻塞编辑 */ + } else { + d->result = (d->type == MEOW_DLG_CONFIRM) ? 1 : 1; + } + d->done = 1; + } + break; + } + } else if (e.type == MEOW_EV_SWIPE && e.a == MEOW_SWIPE_LEFT) { + d->done = 1; d->result = 0; /* 返回=取消 */ + } + } + if (d->done) break; + usleep(16000); + } +} + +int meow_dialog_result(meow_dialog *d) { return d ? d->result : 0; } +int meow_dialog_confirm(meow_dialog *d) +{ + if (!d || d->type != MEOW_DLG_CONFIRM) return 0; + meow_dialog_show(d); + return d->result == 1; +} +void meow_dialog_free(meow_dialog *d) { if (d) free(d); } + +/* ---- BottomSheet ---- */ +struct meow_sheet { + char title[96]; + char items[MEOW_MODAL_MAX_ITEMS][64]; + int nitems; + int h; + int result; + int done; +}; + +static int sheet_h(meow_sheet *s, const meow_theme *th) +{ + int H = meow_ui_height(); + int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0; + int avail = H - sb - 20; + int title_h = meow_ui_text_height(th->font_title); + int min_h = 24 + title_h + 12 + MEOW_MODAL_ROW + 20; + int want = (s->h > 0) ? s->h : min_h; + int est = 24 + title_h + 12 + s->nitems * MEOW_MODAL_ROW + 20; + if (est > want) want = est; + if (want > avail) want = avail; + if (want < min_h) want = min_h < avail ? min_h : avail; + return want; +} + +meow_sheet *meow_sheet_create(const char *title, int h) +{ + meow_sheet *s = (meow_sheet *)calloc(1, sizeof *s); + if (!s) return NULL; + if (title) { strncpy(s->title, title, sizeof s->title - 1); s->title[sizeof s->title - 1] = 0; } + s->h = h > 0 ? h : 0; + return s; +} +int meow_sheet_add_item(meow_sheet *s, const char *label) +{ + if (!s || s->nitems >= MEOW_MODAL_MAX_ITEMS) return -1; + if (label) { strncpy(s->items[s->nitems], label, 63); s->items[s->nitems][63] = 0; } + int idx = s->nitems; + s->nitems++; + return idx; +} +int meow_sheet_open(meow_sheet *s) +{ + if (!s) return -1; + const meow_theme *th = meow_theme_get(); + meow_input_start(); + meow_input_drain(); + s->done = 0; s->result = -1; + int W = meow_ui_width(), H = meow_ui_height(); + int ph = sheet_h(s, th); + int py = H - ph, pad = 24; + /* 首次绘制:遮罩 + 底部面板(顶部圆角) */ + meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); + meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest); + meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface); + for (int i = 0; i < s->nitems; i++) { + int ry = py + pad + meow_ui_text_height(th->font_title) + 12 + i * MEOW_MODAL_ROW; + if (ry + MEOW_MODAL_ROW > H - 8) break; + meow_ui_text(pad, ry + (MEOW_MODAL_ROW - meow_ui_text_height(th->font_body)) / 2, + s->items[i], th->font_body, th->onSurface); + meow_ui_fill_rect(pad, ry + MEOW_MODAL_ROW - 1, W - 2 * pad, 1, th->outlineVariant); + } + meow_ui_flush(); + + while (!s->done) { + meow_input_ev e; + while (meow_input_pop(&e)) { + if (e.type == MEOW_EV_TAP) { + int y = e.b; + if (y < py) { s->done = 1; s->result = -1; break; } /* 点面板外=取消 */ + int title_h = meow_ui_text_height(th->font_title); + int cy = py + pad + title_h + 12; + int idx = (y - cy) / MEOW_MODAL_ROW; + if (idx >= 0 && idx < s->nitems) { s->result = idx; s->done = 1; } + break; + } else if (e.type == MEOW_EV_SWIPE && e.a == MEOW_SWIPE_LEFT) { + s->done = 1; s->result = -1; + } + } + if (s->done) break; + usleep(16000); + } + return s->result; +} +void meow_sheet_free(meow_sheet *s) { if (s) free(s); } diff --git a/meow_ui_gfx.c b/meow_ui_gfx.c index fc359cc..b3b3ba5 100644 --- a/meow_ui_gfx.c +++ b/meow_ui_gfx.c @@ -311,7 +311,6 @@ void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color) { if (!g_init_ok) return; if (w <= 0 || h <= 0) return; - int x0 = x, y0 = y; if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x >= g_xres || y >= g_yres) return; @@ -394,6 +393,105 @@ void meow_ui_draw_rect_round(int x, int y, int w, int h, int r, uint32_t color) dirty_add(x1, y1, x2 - x1, y2 - y1); } +/* ================= §5.8.6 batch2:圆形/线段/圆弧环(软渲,无 -lm) ================= + * 无 GPU 合成、构建只链 libz+pthread(无 -lm),故不用 math.h:COS 用象限对称查表、 + * 圆用 bbox 距离测试、弧带用「沿弧采样成粗线(圆帽)」近似。 */ +static const int m_cos90[91] = { + 1024,1024,1023,1023,1022,1020,1018,1016, + 1014,1011,1008,1005,1002,998,994,989, + 984,979,974,968,962,956,949,943, + 935,928,920,912,904,896,887,878, + 868,859,849,839,828,818,807,796, + 784,773,761,749,737,724,711,698, + 685,672,658,644,630,616,602,587, + 573,558,543,527,512,496,481,465, + 449,433,416,400,384,367,350,333, + 316,299,282,265,248,230,213,195, + 178,160,143,125,107,89,71,54, + 36,18,0, +}; + +static int m_cos_deg(int deg) /* 任意角度(可负)→ cos(deg)*1024 */ +{ + int a = deg % 360; + if (a < 0) a += 360; + if (a <= 90) return m_cos90[a]; + if (a <= 180) return -m_cos90[180 - a]; + if (a <= 270) return -m_cos90[a - 180]; + return m_cos90[360 - a]; +} +static int m_sin_deg(int deg) { return m_cos_deg(90 - deg); } + +void meow_gfx_fill_circle(int cx, int cy, int r, uint32_t color) +{ + if (!g_init_ok || r <= 0) return; + int x1 = cx - r, y1 = cy - r, x2 = cx + r + 1, y2 = cy + r + 1; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + if (x2 <= x1 || y2 <= y1) return; + int r2 = r * r; + for (int py = y1; py < y2; py++) { + int dy = py - cy; + for (int px = x1; px < x2; px++) { + int dx = px - cx; + if (dx * dx + dy * dy <= r2) put_px(px, py, color); + } + } + dirty_add(x1, y1, x2 - x1, y2 - y1); +} + +void meow_gfx_line(int x0, int y0, int x1, int y1, int th, uint32_t color) +{ + if (!g_init_ok) return; + int dx = x1 - x0, dy = y1 - y0; + int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + int steps = (adx > ady ? adx : ady) + 1; + if (steps <= 1) { meow_gfx_fill_circle(x0, y0, th / 2 + 1, color); return; } + int cap = th / 2; + if (cap < 1) cap = 1; + for (int i = 0; i <= steps; i++) { + int x = x0 + dx * i / steps; + int y = y0 + dy * i / steps; + if (cap <= 1) put_px(x, y, color); + else meow_gfx_fill_circle(x, y, cap, color); + } +} + +void meow_gfx_ring_arc(int cx, int cy, int r, int th, int start_deg, int sweep_deg, uint32_t color) +{ + if (!g_init_ok || r <= 0) return; + int sw = sweep_deg % 360; + if (sw < 0) sw += 360; + if (sw >= 360) { /* 整环:bbox 距离带(无角度判定,最快) */ + int r1 = r - th / 2, r2 = r + th / 2; + if (r1 < 0) r1 = 0; + int x1 = cx - r2, y1 = cy - r2, x2 = cx + r2 + 1, y2 = cy + r2 + 1; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + if (x2 <= x1 || y2 <= y1) return; + for (int py = y1; py < y2; py++) + for (int px = x1; px < x2; px++) { + int dx = px - cx, dy = py - cy; + int d2 = dx * dx + dy * dy; + if (d2 >= r1 * r1 && d2 <= r2 * r2) put_px(px, py, color); + } + dirty_add(x1, y1, x2 - x1, y2 - y1); + return; + } + /* 弧带:沿弧采样相邻点、粗线连接(圆帽)近似。方向:deg 右(0)→上(90)→左(180)→下(270),顺时针。 */ + int n = sw / 2 + 1; + if (n < 2) n = 2; + int px = cx + (m_cos_deg(start_deg) * r) / 1024; + int py = cy - (m_sin_deg(start_deg) * r) / 1024; /* 屏幕 y 向下 → sin 取负 */ + for (int i = 1; i <= n; i++) { + int deg = start_deg + sw * i / n; + int nx = cx + (m_cos_deg(deg) * r) / 1024; + int ny = cy - (m_sin_deg(deg) * r) / 1024; + meow_gfx_line(px, py, nx, ny, th, color); + px = nx; py = ny; + } +} + void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha) { if (!g_init_ok) return; diff --git a/meow_ui_internal.h b/meow_ui_internal.h index a38a059..9a4a610 100644 --- a/meow_ui_internal.h +++ b/meow_ui_internal.h @@ -112,6 +112,13 @@ unsigned char *meow_ui_back_buf(void); /* 实心圆角矩形(控件按钮底色用;公共 §3 只有空心 draw_rect_round)。[D3] */ void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color); +/* §5.8.6 batch2:软渲圆形/线段/圆弧环原语(无 GPU、无 -lm,内部整数/查表实现)。 + * 供 Radio(外圈/内点)、Badge(红点)、ProgressBar(环形轨道/进度弧)、Checkbox(勾线段)使用。 */ +void meow_gfx_fill_circle(int cx, int cy, int r, uint32_t color); /* 实心圆(bbox 距离测试) */ +void meow_gfx_line(int x0, int y0, int x1, int y1, int th, uint32_t color); /* 粗线(Bresenham + 圆帽) */ +void meow_gfx_ring_arc(int cx, int cy, int r, int th, int start_deg, + int sweep_deg, uint32_t color); /* 圆弧环(查表采样成粗线) */ + /* 是否运行于真实 fb 之上(0 = 头less/离屏,qemu/宿主)。 */ int meow_gfx_is_device(void); diff --git a/meow_ui_widget.c b/meow_ui_widget.c index dd5d8e0..f4f5bb3 100644 --- a/meow_ui_widget.c +++ b/meow_ui_widget.c @@ -8,13 +8,20 @@ #include "meow_ui_internal.h" #include #include +#include enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3, - W_CUSTOM = 4, W_LAYOUT = 5, W_SPACER = 6 }; + 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 }; /* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */ #define MEOW_LAYOUT_MAX_CHILD 32 +/* 容器(TopAppBar 动作 / NavigationBar 项 / TabBar 页签)子项上限。 */ +#define MEOW_CONTAINER_MAX 8 + /* 按钮「按住」(长按)判定阈值(ms):超过即进入更强态(更深状态层)。 * 视觉区分「短按按下」与「按住」;触发语义不变(on_tap 仍按抬手判定)。 */ #define MEOW_BTN_HOLD_MS 500u @@ -51,6 +58,21 @@ struct meow_widget { int spacing; /* W_LAYOUT:项间间距 */ meow_widget *children[MEOW_LAYOUT_MAX_CHILD]; /* W_LAYOUT:子控件 */ int nchildren; + + /* §5.8.6 batch2 Material 子集(尾缀追加;calloc 归零,安全) */ + int variant; /* W_BTN:MEOW_BTN_* 变体; W_PROGRESS:0=线性/1=圆形 */ + char placeholder[64]; /* W_TEXTFIELD 提示文案 */ + char *buf; int buflen; /* W_TEXTFIELD 文本缓冲(App 持有)+ 当前长 */ + int maxlen; /* W_TEXTFIELD 上限(含 NUL) */ + meow_ime_mode ime_mode; /* W_TEXTFIELD IME 模式 */ + int value; /* W_PROGRESS:0-100 / -1=不定; W_BADGE:计数 */ + int item_h; /* W_LIST:行高/列宽(lazy),默认 44 */ + int scroll; /* W_LIST:滚动偏移(lazy) */ + int horizontal; /* W_LIST:1=横向(LazyRow) */ + int back_on; /* W_TOPAPPBAR:显示返回箭头 */ + char items[MEOW_CONTAINER_MAX][24]; /* W_TOPAPPBAR 动作 / W_NAVBAR 项 / W_TABBAR 页签 */ + int nitems; /* 上述项数 */ + int selected; /* W_NAVBAR / W_TABBAR 选中下标 */ }; /* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)---- @@ -391,6 +413,384 @@ void meow_layout_arrange(meow_widget *layout) } } +/* ============================================================ */ +/* §5.8.6 batch2 Material 组件子集 API(create/setter/getter/hit) */ +/* ============================================================ */ + +/* ---- Button 五型(既有 meow_btn_create = FILLED) ---- */ +meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *label, int variant) +{ + meow_widget *t = meow_btn_create(x, y, w, h, label); + if (!t) return NULL; + if (variant < MEOW_BTN_FILLED || variant > MEOW_BTN_ELEVATED) variant = MEOW_BTN_FILLED; + t->variant = variant; + return t; +} +int meow_btn_set_variant(meow_widget *w, int variant) +{ + if (!w || w->type != W_BTN) return -1; + if (variant < MEOW_BTN_FILLED || variant > MEOW_BTN_ELEVATED) return -1; + if (variant == w->variant) return 0; + w->variant = variant; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +/* ---- TextField ---- */ +meow_widget *meow_textfield_create(int x, int y, int w, int h, const char *placeholder, + char *buf, int maxlen, meow_ime_mode mode) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_TEXTFIELD; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + if (placeholder) { + strncpy(t->placeholder, placeholder, sizeof t->placeholder - 1); + t->placeholder[sizeof t->placeholder - 1] = 0; + } + t->buf = buf; t->buflen = buf ? (int)strlen(buf) : 0; + t->maxlen = (maxlen > 0 && maxlen <= 256) ? maxlen : 256; + t->ime_mode = mode; + return t; +} +void meow_textfield_set_placeholder(meow_widget *w, const char *placeholder) +{ + if (!w || w->type != W_TEXTFIELD) return; + if (placeholder) { + strncpy(w->placeholder, placeholder, sizeof w->placeholder - 1); + w->placeholder[sizeof w->placeholder - 1] = 0; + } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +const char *meow_textfield_text(meow_widget *w) { return (w && w->type == W_TEXTFIELD && w->buf) ? w->buf : ""; } +/* 输入中实时把 IME 文本同步到字段缓冲 + 只刷字段区(字段在顶部,IME 键盘在底部覆盖层) */ +static void textfield_change_cb(const char *text, void *ctx) +{ + meow_widget *w = (meow_widget *)ctx; + if (!w || w->type != W_TEXTFIELD) return; + if (w->buf && text) { + strncpy(w->buf, text, w->maxlen - 1); + w->buf[w->maxlen - 1] = 0; + } + w->buflen = text ? (int)strlen(text) : 0; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_textfield_activate(meow_widget *w) +{ + if (!w || w->type != W_TEXTFIELD) return 0; + w->state |= MEOW_WS_FOCUSED; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + meow_ime_ctx ctx; + memset(&ctx, 0, sizeof ctx); + ctx.mode = w->ime_mode; + ctx.maxlen = w->maxlen; + ctx.on_change = textfield_change_cb; + ctx.ctx = w; + if (w->buf) strncpy(ctx.buf, w->buf, sizeof ctx.buf - 1); /* 预填当前文本 */ + int r = meow_ui_text_input(&ctx); + if (r && w->buf) { + strncpy(w->buf, ctx.buf, w->maxlen - 1); + w->buf[w->maxlen - 1] = 0; + } + w->buflen = w->buf ? (int)strlen(w->buf) : 0; + w->state &= ~MEOW_WS_FOCUSED; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return r; +} + +/* ---- Card ---- */ +meow_widget *meow_card_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_CARD; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + return t; +} +void meow_card_set_content(meow_widget *card, meow_widget *child) +{ + if (!card || card->type != W_CARD || !child) return; + card->children[0] = child; + card->nchildren = 1; +} + +/* ---- Divider ---- */ +meow_widget *meow_divider_create(int x, int y, int len, int vertical) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_DIVIDER; t->x = x; t->y = y; + t->w = vertical ? 1 : (len > 0 ? len : 1); + t->h = vertical ? (len > 0 ? len : 1) : 1; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->variant = vertical ? 1 : 0; + return t; +} + +/* ---- Badge ---- */ +meow_widget *meow_badge_create(int cx, int cy) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_BADGE; t->x = cx; t->y = cy; t->w = 24; t->h = 24; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->value = 0; /* 红点 */ + return t; +} +void meow_badge_set(meow_widget *w, int count) +{ + if (!w || w->type != W_BADGE) return; + w->value = count; + meow_gfx_mark_dirty(w->x - 16, w->y - 16, 32, 32); +} + +/* ---- Checkbox / Radio ---- */ +meow_widget *meow_checkbox_create(int x, int y) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_CHECKBOX; t->x = x; t->y = y; t->w = 34; t->h = 34; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + return t; +} +meow_widget *meow_radio_create(int x, int y) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_RADIO; t->x = x; t->y = y; t->w = 34; t->h = 34; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + return t; +} + +/* ---- ProgressBar ---- */ +meow_widget *meow_progress_create(int x, int y, int w, int h, int value) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_PROGRESS; t->x = x; t->y = y; t->w = w; t->h = h > 0 ? h : 4; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->variant = 0; /* 线性 */ + t->value = value < 0 ? -1 : (value > 100 ? 100 : value); + return t; +} +meow_widget *meow_progress_circular_create(int cx, int cy, int r, int value) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_PROGRESS; t->x = cx; t->y = cy; t->w = r; t->h = r; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->variant = 1; /* 圆形 */ + t->value = value < 0 ? -1 : (value > 100 ? 100 : value); + return t; +} +void meow_progress_set(meow_widget *w, int value) +{ + if (!w || w->type != W_PROGRESS) return; + w->value = value < 0 ? -1 : (value > 100 ? 100 : value); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} + +/* ---- FAB ---- */ +meow_widget *meow_fab_create(int x, int y, int size, const char *label) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_FAB; t->x = x; t->y = y; t->w = size > 0 ? size : 48; t->h = t->w; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + if (label) { strncpy(t->label, label, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } + return t; +} + +/* ---- TopAppBar ---- */ +meow_widget *meow_topappbar_create(int x, int y, int w, int h, const char *title) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_TOPAPPBAR; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->back_on = 0; t->nitems = 0; + if (title) { strncpy(t->label, title, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } + return t; +} +void meow_topappbar_set_title(meow_widget *w, const char *title) +{ + if (!w || w->type != W_TOPAPPBAR) return; + if (title) { strncpy(w->label, title, sizeof w->label - 1); w->label[sizeof w->label - 1] = 0; } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +void meow_topappbar_set_back(meow_widget *w, int on) +{ + if (!w || w->type != W_TOPAPPBAR) return; + w->back_on = on ? 1 : 0; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_topappbar_add_action(meow_widget *w, const char *label) +{ + if (!w || w->type != W_TOPAPPBAR || w->nitems >= MEOW_CONTAINER_MAX) return -1; + if (label) strncpy(w->items[w->nitems], label, 23); + w->items[w->nitems][23] = 0; + int idx = w->nitems; + w->nitems++; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return idx; +} +int meow_topappbar_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_TOPAPPBAR) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + if (w->back_on && x >= w->x && x < w->x + 44) return MEOW_TOPBAR_BACK; + const meow_theme *th = meow_theme_get(); + int ax = w->x + w->w - 10; + for (int i = w->nitems - 1; i >= 0; i--) { + int aw = meow_ui_text_width(w->items[i], th->font_body) + 12; + ax -= aw; + if (x >= ax && x < ax + aw && x >= w->x + 100) return i; /* 不压标题区/返回区 */ + } + return -1; +} + +/* ---- NavigationBar ---- */ +meow_widget *meow_navigationbar_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_NAVBAR; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->nitems = 0; t->selected = 0; + return t; +} +int meow_navigationbar_add_item(meow_widget *w, const char *label) +{ + if (!w || w->type != W_NAVBAR || w->nitems >= MEOW_CONTAINER_MAX) return -1; + if (label) strncpy(w->items[w->nitems], label, 23); + w->items[w->nitems][23] = 0; + int idx = w->nitems; + w->nitems++; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return idx; +} +void meow_navigationbar_set_selected(meow_widget *w, int idx) +{ + if (!w || w->type != W_NAVBAR) return; + if (idx < 0) idx = 0; + if (idx >= w->nitems) idx = w->nitems - 1; + w->selected = idx; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_navigationbar_selected(meow_widget *w) { return (w && w->type == W_NAVBAR) ? w->selected : 0; } +int meow_navigationbar_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_NAVBAR || w->nitems <= 0) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int iw = w->w / w->nitems; + int idx = iw > 0 ? (x - w->x) / iw : 0; + return (idx >= 0 && idx < w->nitems) ? idx : -1; +} + +/* ---- TabBar ---- */ +meow_widget *meow_tabbar_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_TABBAR; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->nitems = 0; t->selected = 0; + return t; +} +int meow_tabbar_add_tab(meow_widget *w, const char *label) +{ + if (!w || w->type != W_TABBAR || w->nitems >= MEOW_CONTAINER_MAX) return -1; + if (label) strncpy(w->items[w->nitems], label, 23); + w->items[w->nitems][23] = 0; + int idx = w->nitems; + w->nitems++; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return idx; +} +void meow_tabbar_set_selected(meow_widget *w, int idx) +{ + if (!w || w->type != W_TABBAR) return; + if (idx < 0) idx = 0; + if (idx >= w->nitems) idx = w->nitems - 1; + w->selected = idx; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_tabbar_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_TABBAR || w->nitems <= 0) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int iw = w->w / w->nitems; + int idx = iw > 0 ? (x - w->x) / iw : 0; + return (idx >= 0 && idx < w->nitems) ? idx : -1; +} + +/* ---- 列表容器(LazyColumn/LazyRow:虚拟化 + 滚动;复用 W_LIST 与 meow_list_source)---- */ +meow_widget *meow_lazycolumn_create(int x, int y, int w, int h, int item_h, int count, + meow_list_source src, void *ctx) +{ + meow_widget *t = meow_list_create(x, y, w, h, count, src, ctx); + if (!t) return NULL; + t->item_h = item_h > 0 ? item_h : 44; + t->scroll = 0; t->horizontal = 0; + return t; +} +meow_widget *meow_lazyrow_create(int x, int y, int w, int h, int item_w, int count, + meow_list_source src, void *ctx) +{ + meow_widget *t = meow_list_create(x, y, w, h, count, src, ctx); + if (!t) return NULL; + t->item_h = item_w > 0 ? item_w : 60; + t->scroll = 0; t->horizontal = 1; + return t; +} +static int list_item(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 44; } +int meow_list_max_scroll(meow_widget *w) +{ + if (!w || w->type != W_LIST) return 0; + int vp = w->horizontal ? w->w : w->h; + int ms = w->count * list_item(w) - vp; + return ms < 0 ? 0 : ms; +} +void meow_list_set_scroll(meow_widget *w, int offset) +{ + if (!w || w->type != W_LIST) return; + int maxs = meow_list_max_scroll(w); + if (offset < 0) offset = 0; + if (offset > maxs) offset = maxs; + if (offset == w->scroll) return; + w->scroll = offset; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_list_scroll(meow_widget *w) { return (w && w->type == W_LIST) ? w->scroll : 0; } +void meow_list_scroll_by(meow_widget *w, int dy) { if (w) meow_list_set_scroll(w, w->scroll + dy); } +int meow_list_set_item_h(meow_widget *w, int item) +{ + if (!w || w->type != W_LIST) 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; +} +int meow_list_hit_index(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_LIST) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int item = list_item(w); + int idx = w->horizontal ? (x - w->x + w->scroll) / item + : (y - w->y + w->scroll) / item; + return (idx >= 0 && idx < w->count) ? idx : -1; +} + static int hit(meow_widget *w, int x, int y) { if (!w) return 0; @@ -453,27 +853,50 @@ void meow_widget_draw(meow_widget *w) int press = (w->state & MEOW_WS_PRESSED) || w->pressed || live; int hold = live && meow_input_touch_hold_ms() >= MEOW_BTN_HOLD_MS; if (disabled) press = 0; - /* 底色:选中态用 primaryContainer(onPrimaryContainer 文字);否则 primary(onPrimary 文字)。 - * 可被 opts.bg 覆盖(此时文字用 opts.fg 或 onPrimary)。 */ + /* §5.8.6 五型:filled=primary/onPrimary;tonal=secondaryContainer/onSecondaryContainer; + * outlined=透明底+primary 文+outline 描边;text=透明底+primary 文;elevated=surfaceContainerLow+primary 文。 + * 选中态优先生效(primaryContainer / onPrimaryContainer);opts.bg/fg 可覆盖。 */ int selected = (w->state & MEOW_WS_SELECTED); - uint32_t base, default_fg; - if (selected && !(w->opts_used && w->opts.bg)) { base = th->primaryContainer; default_fg = th->onPrimaryContainer; } - else { base = widget_bg(w, th, th->primary); default_fg = th->onPrimary; } + int variant = w->variant; + if (variant < MEOW_BTN_FILLED || variant > MEOW_BTN_ELEVATED) variant = MEOW_BTN_FILLED; + int fill = 1, stroke_draw = 0, stroke_w = 1; + uint32_t base, default_fg, stroke_col = 0; + if (selected && !(w->opts_used && w->opts.bg)) { + base = th->primaryContainer; default_fg = th->onPrimaryContainer; + } else { + switch (variant) { + case MEOW_BTN_TONAL: + base = th->secondaryContainer; default_fg = th->onSecondaryContainer; break; + case MEOW_BTN_OUTLINED: + fill = 0; base = 0; default_fg = th->primary; + stroke_draw = 1; stroke_col = th->outline; stroke_w = 1; break; + case MEOW_BTN_TEXT: + fill = 0; base = 0; default_fg = th->primary; break; + case MEOW_BTN_ELEVATED: + base = th->surfaceContainerLow; default_fg = th->primary; + stroke_draw = 1; stroke_col = th->outlineVariant; stroke_w = 1; break; + default: + base = th->primary; default_fg = th->onPrimary; break; + } + } + if (w->opts_used && w->opts.bg) { base = w->opts.bg; fill = 1; } + if (w->opts_used && w->opts.fg) default_fg = w->opts.fg; uint32_t bg; - if (hold) bg = state_tint(base, th, 24); /* 按住 = 深状态层 */ - else if (press) bg = state_tint(base, th, 12); /* 按下 = 状态层 */ - else bg = base; + if (fill) bg = (hold) ? state_tint(base, th, 24) : (press ? state_tint(base, th, 12) : base); + else bg = 0; int r = widget_radius(w, 10); - meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, bg); + if (fill) meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, bg); + else if (press) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->primary, 12); /* transparent ripple */ /* 内容区受 pad 垂直内缩(水平居中文本,pad_x 不影响中心) */ int pady = widget_pady(w); int cx = w->x + w->w / 2; int cy = w->y + pady + (w->h - 2 * pady - meow_ui_text_height(th->font_body)) / 2; - uint32_t tc = widget_fg(w, th, default_fg); + uint32_t tc = default_fg; meow_ui_text_center(cx, cy, w->label, th->font_body, tc); - /* opts 描边 / 焦点外圈 */ - if (w->opts_used && w->opts.stroke && w->opts.stroke_w > 0) - meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, w->opts.stroke); + /* opts 描边(优先)/ 变体内建描边 / 焦点外圈 */ + int sw = (w->opts_used && w->opts.stroke && w->opts.stroke_w > 0) ? w->opts.stroke_w : (stroke_draw ? stroke_w : 0); + uint32_t sc = (w->opts_used && w->opts.stroke) ? w->opts.stroke : stroke_col; + if (sw > 0 && sc) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, sc); if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); /* 禁用:整体变暗(状态层向 onSurface 叠 α) */ @@ -503,18 +926,41 @@ void meow_widget_draw(meow_widget *w) break; } case W_LIST: { - int rowh = 44, pad = 16; - for (int i = 0; i < w->count; i++) { - int ry = w->y + i * rowh; - if (ry >= w->y + w->h) break; - if (i == w->sel) meow_ui_fill_rect(w->x, ry, w->w, rowh, th->primary); + /* §5.8.6 列表容器(LazyColumn/LazyRow):只画可见项(虚拟化)+ 滚动偏移。 + * 纵向=行高 item_h(默认 44,兼容既有 meow_list_create);横向=LazyRow(列宽=item_h)。 */ + int pad = 16; + int item = list_item(w); + int vp = w->horizontal ? w->w : w->h; + int off = w->scroll; + if (off < 0) off = 0; + int maxs = w->count * item - vp; /* max_scroll(与 meow_list_max_scroll 同式) */ + if (maxs < 0) maxs = 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; + for (int i = first; i < last; i++) { char label[64] = ""; - if (w->src && w->src(i, label, (int)sizeof label, w->ctx)) { + if (!w->src || !w->src(i, label, (int)sizeof label, w->ctx)) continue; + int pos = i * item - off; /* 相对容器起点的可视位置 */ + if (w->horizontal) { + int rx = w->x + pos; + if (rx >= w->x + w->w) break; + if (i == w->sel) meow_ui_fill_rect(rx, w->y, item, w->h, th->primary); uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface; - meow_ui_text(w->x + pad, ry + (rowh - meow_ui_text_height(th->font_body)) / 2, + meow_ui_text_center(rx + item / 2, w->y + (w->h - meow_ui_text_height(th->font_body)) / 2, + label, th->font_body, col); + meow_ui_fill_rect(rx + item - 1, w->y, 1, w->h, th->onSurfaceVariant); + } else { + int ry = w->y + pos; + if (ry >= w->y + w->h) break; + if (i == w->sel) meow_ui_fill_rect(w->x, ry, w->w, item, th->primary); + uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface; + meow_ui_text(w->x + pad, ry + (item - meow_ui_text_height(th->font_body)) / 2, label, th->font_body, col); + meow_ui_fill_rect(w->x, ry + item - 1, w->w, 1, th->onSurfaceVariant); } - meow_ui_fill_rect(w->x, ry + rowh - 1, w->w, 1, th->onSurfaceVariant); } if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; @@ -529,6 +975,180 @@ void meow_widget_draw(meow_widget *w) for (int i = 0; i < w->nchildren; i++) if (w->children[i]) meow_widget_draw(w->children[i]); break; + case W_TEXTFIELD: { + /* MD3 输入框:圆角外框(surfaceContainerLowest 底 + outline 描边;focused=primary 2px)+ 提示/文本 + 光标。 */ + int r = widget_radius(w, 8); + int focused = (w->state & MEOW_WS_FOCUSED) != 0; + meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, th->surfaceContainerLowest); + uint32_t bcol = focused ? th->primary : th->outline; + int bw = focused ? 2 : 1; + if (bw >= 2) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, bcol); + else meow_ui_draw_rect_round(w->x + 1, w->y + 1, w->w - 2, w->h - 2, + r > 1 ? r - 1 : 0, bcol); + int has = w->buf && w->buf[0]; + uint32_t col = has ? th->onSurface : th->onSurfaceVariant; + const char *s = has ? w->buf : (w->placeholder[0] ? w->placeholder : ""); + int tx = w->x + 12; + int ty = w->y + (w->h - meow_ui_text_height(th->font_body)) / 2; + meow_ui_text(tx, ty, s, th->font_body, col); + if (focused && has) { + int tw = meow_ui_text_width(w->buf, th->font_body); + meow_ui_fill_rect(tx + tw, ty, 2, meow_ui_text_height(th->font_body), th->primary); + } + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_CARD: { + int r = widget_radius(w, th->radius_card); + uint32_t base = widget_bg(w, th, th->surfaceContainerLow); + meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, base); + if (w->opts_used && w->opts.stroke && w->opts.stroke_w > 0) + meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, w->opts.stroke); + if (w->state & MEOW_WS_SELECTED) + meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->primary, 24); + if (w->nchildren > 0 && w->children[0]) meow_widget_draw(w->children[0]); + if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_DIVIDER: { + uint32_t c = widget_bg(w, th, th->outlineVariant); + if (w->variant) meow_ui_fill_rect(w->x, w->y, 1, w->h, c); + else meow_ui_fill_rect(w->x, w->y, w->w, 1, c); + break; + } + case W_BADGE: { + int count = w->value; + if (count < 0) break; /* 隐藏 */ + uint32_t bgc = th->error; /* MD3 角标用 error */ + if (count == 0) { + meow_gfx_fill_circle(w->x, w->y, 5, bgc); + } else { + char s[16]; + snprintf(s, sizeof s, "%d", count); + int tw = meow_ui_text_width(s, th->font_body); + int pw = tw + 12; if (pw < 18) pw = 18; + meow_gfx_fill_round(w->x - pw / 2, w->y - 11, pw, 22, 11, bgc); + meow_ui_text_center(w->x, w->y - meow_ui_text_height(th->font_body) / 2, s, th->font_body, th->onError); + } + break; + } + case W_CHECKBOX: { + int sel = (w->state & MEOW_WS_SELECTED) != 0; + int sz = w->w; + int r = widget_radius(w, 4); + if (sel) { + meow_gfx_fill_round(w->x, w->y, sz, sz, r, th->primary); + int ax = w->x + 9, ay1 = w->y + 17, ay2 = w->y + 23, bx = w->x + 15, cx = w->x + 25, cy = w->y + 11; + meow_gfx_line(ax, ay1, bx, ay2, 3, th->onPrimary); + meow_gfx_line(bx, ay2, cx, cy, 3, th->onPrimary); + } else { + meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->outline); + } + if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->primary); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, sz, sz, th->onSurface, 90); + break; + } + case W_RADIO: { + int sel = (w->state & MEOW_WS_SELECTED) != 0; + int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2; + meow_gfx_ring_arc(ccx, ccy, 12, 2, 0, 360, sel ? th->primary : th->outline); + if (sel) meow_gfx_fill_circle(ccx, ccy, 6, th->primary); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_PROGRESS: { + if (w->variant == 1) { /* 圆形 */ + int ccx = w->x, ccy = w->y, rr = w->w; + int thk = 4; + meow_gfx_ring_arc(ccx, ccy, rr, thk, 0, 360, th->surfaceVariant); + if (w->value < 0) { /* 不定:按时间推进一段弧(App 在 on_tick 里重绘驱动) */ + int start = (int)((meow_ui_time_ms() / 30) % 360); + meow_gfx_ring_arc(ccx, ccy, rr, thk, start - 90, 90, th->primary); + } else if (w->value > 0) { + int sweep = w->value * 360 / 100; + if (sweep > 0) meow_gfx_ring_arc(ccx, ccy, rr, thk, -90, sweep, th->primary); + } + } else { /* 线性 */ + int thk = w->h > 0 ? w->h : 4; + int yy = w->y + w->h / 2 - thk / 2; + meow_ui_fill_rect(w->x, yy, w->w, thk, th->surfaceVariant); + if (w->value < 0) { /* 不定:移动块 */ + int fw = w->w / 3; + int pos = (int)((meow_ui_time_ms() / 12) % (w->w + fw)) - fw; + meow_ui_fill_rect(w->x + pos, yy, fw, thk, th->primary); + } else if (w->value > 0) { + int fw = w->value * w->w / 100; + if (fw > 0) meow_ui_fill_rect(w->x, yy, fw, thk, th->primary); + } + } + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_FAB: { + int r = widget_radius(w, w->w / 2); + uint32_t base = widget_bg(w, th, th->primaryContainer); + if (w->state & MEOW_WS_PRESSED) base = state_tint(base, th, 12); + meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, base); + uint32_t tc = widget_fg(w, th, th->onPrimaryContainer); + meow_ui_text_center(w->x + w->w / 2, w->y + (w->h - meow_ui_text_height(th->font_body)) / 2, + w->label, th->font_body, tc); + if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_TOPAPPBAR: { + meow_ui_fill_rect(w->x, w->y, w->w, w->h, th->surfaceContainerHigh); + int mid = w->y + w->h / 2; + int tx = w->x + 16; + int ty = mid - meow_ui_text_height(th->font_title) / 2; + if (w->back_on) { + int bx = w->x + 22, by = mid; + meow_gfx_line(bx + 4, by - 7, bx - 4, by, 2, th->onSurface); + meow_gfx_line(bx - 4, by, bx + 4, by + 7, 2, th->onSurface); + tx = w->x + 48; + } + if (w->label[0]) meow_ui_text(tx, ty, w->label, th->font_title, th->onSurface); + int ay = mid - meow_ui_text_height(th->font_body) / 2; + int ax = w->x + w->w - 12; + for (int i = w->nitems - 1; i >= 0; i--) { + int aw = meow_ui_text_width(w->items[i], th->font_body) + 12; + ax -= aw; + meow_ui_text_center(ax + aw / 2, ay, w->items[i], th->font_body, th->onSurface); + } + meow_ui_fill_rect(w->x, w->y + w->h - 1, w->w, 1, th->outlineVariant); + break; + } + case W_NAVBAR: { + meow_ui_fill_rect(w->x, w->y, w->w, w->h, th->surfaceContainer); + if (w->nitems <= 0) break; + int iw = w->w / w->nitems; + int ay = w->y + (w->h - meow_ui_text_height(th->font_body)) / 2 + 12; + for (int i = 0; i < w->nitems; i++) { + int ix = w->x + i * iw; + int sel = (i == w->selected); + if (sel) meow_gfx_fill_round(ix + iw / 2 - 24, w->y + 8, 48, 22, 11, th->secondaryContainer); + uint32_t col = sel ? th->onSecondaryContainer : th->onSurfaceVariant; + meow_ui_text_center(ix + iw / 2, ay, w->items[i], th->font_body, col); + } + meow_ui_fill_rect(w->x, w->y, w->w, 1, th->outlineVariant); + break; + } + case W_TABBAR: { + meow_ui_fill_rect(w->x, w->y, w->w, w->h, th->surface); + if (w->nitems <= 0) break; + int iw = w->w / w->nitems; + int ay = w->y + (w->h - meow_ui_text_height(th->font_body)) / 2; + for (int i = 0; i < w->nitems; i++) { + int ix = w->x + i * iw; + int sel = (i == w->selected); + meow_ui_text_center(ix + iw / 2, ay, w->items[i], th->font_body, + sel ? th->primary : th->onSurfaceVariant); + if (sel) meow_ui_fill_rect(ix, w->y + w->h - 2, iw, 2, th->primary); + } + meow_ui_fill_rect(w->x, w->y + w->h - 1, w->w, 1, th->outlineVariant); + break; + } case W_SPACER: break; /* 不可见空白 */ default: diff --git a/test_host_stub.c b/test_host_stub.c new file mode 100644 index 0000000..da41ebf --- /dev/null +++ b/test_host_stub.c @@ -0,0 +1,24 @@ +/* test_host_stub.c — 宿主逻辑测试打桩:meow_font_* / meow_ui_global_render。 + * 供 test_sb_collapse.c 链真实 meow_ui_gfx.o/meow_ui_sys.o 时提供(无 /dev/fb0、无字库的宿主环境)。 + * 仅为宿主单测编译;不进 meow_ui.a(build_sdk.sh 的 OBJS 不含本文件)。 */ +#include "meow_ui.h" +#include "meow_font.h" /* 原型(meow_ui_gfx.c include 的同款头) */ +#include + +void meow_font_set_fb(unsigned w, unsigned h, unsigned pitch) { (void)w;(void)h;(void)pitch; } +int meow_font_load(const char *p, const char *i) { (void)p;(void)i; return -1; } /* 无库:g_mfont_ready=0 → 5x7 回退 */ +void meow_font_unload(void) {} +int meow_font_reload_tier(int t, const char *p, const char *i) { (void)t;(void)p;(void)i; return -1; } +void meow_font_drawtext(uint16_t *fb, int x, int y, const char *s, uint16_t c, int size) { (void)fb;(void)x;(void)y;(void)s;(void)c;(void)size; } +int meow_font_width_text(const char *s, int size) { (void)size; return (int)strlen(s) * 6; } +int meow_font_line_height(int size) { (void)size; return 20; } +int meow_font_config_multifont(int on, const char *gp, const char *gi, + const meow_font_class_cfg cfg[MEOW_FCLS_COUNT], + int *distinct, int *truncated) +{ (void)on;(void)gp;(void)gi;(void)cfg;(void)distinct;(void)truncated; return -1; } + +/* present() 的 App 跨 Activity 全局渲染骨架(宿主无控件,no-op) */ +void meow_ui_global_render(void) {} + +/* meow_ui_run 触摸分流调用 meow_ui_global_hit(全局控件命中;宿主无全局控件 → 不消费) */ +int meow_ui_global_hit(int x, int y) { (void)x; (void)y; return 0; } diff --git a/test_meow_ui_widgets.c b/test_meow_ui_widgets.c new file mode 100644 index 0000000..168ef5b --- /dev/null +++ b/test_meow_ui_widgets.c @@ -0,0 +1,180 @@ +/* test_meow_ui_widgets.c — 宿主验证 §5.8.6 Material 子集纯逻辑(create/hit/state/滚动/选中)+ + * 各组件绘制(5x7 字库兜底)dump PPM 目检。不依赖 /dev/fb0/字库(meow_font 打桩)。 + * + * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): + * 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 \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread + * 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕) + */ +#include "meow_ui.h" +#include +#include +#include +#include + +/* 测试数据源(列表容器) */ +static int list_src(int idx, char *label, int len, void *ctx) +{ + (void)ctx; + if (idx < 0 || idx >= 100) return 0; + snprintf(label, (size_t)len, "item %d", idx); + return 1; +} + +static meow_widget *g_w[64]; +static int g_wn = 0; +static meow_widget *W(meow_widget *w) { g_w[g_wn++] = w; return w; } + +static void free_all(void) +{ + for (int i = 0; i < g_wn; i++) meow_widget_free(g_w[i]); + g_wn = 0; +} + +int main(void) +{ + setenv("MEOW_UI_DUMP", "/tmp/b2.ppm", 1); + meow_ui_opts o = { 480, 720 }; + assert(meow_ui_init(&o) == 0); + int xres = meow_ui_width(), yres = meow_ui_height(); + assert(xres == 480 && yres == 720); + + /* ---------- Button 五型:create_variant/set_variant + 命中 ---------- */ + meow_widget *bf = W(meow_btn_create_variant(10, 10, 200, 56, "FILLED", MEOW_BTN_FILLED)); + meow_widget *bt = W(meow_btn_create_variant(10, 80, 200, 56, "TONAL", MEOW_BTN_TONAL)); + meow_widget *bo = W(meow_btn_create_variant(10, 150, 200, 56, "OUTLINED", MEOW_BTN_OUTLINED)); + meow_widget *bx = W(meow_btn_create_variant(10, 220, 200, 56, "TEXT", MEOW_BTN_TEXT)); + meow_widget *be = W(meow_btn_create_variant(10, 290, 200, 56, "ELEV", MEOW_BTN_ELEVATED)); + assert(meow_btn_set_variant(bt, MEOW_BTN_TONAL) == 0); /* 同值 */ + assert(meow_btn_set_variant(bo, MEOW_BTN_OUTLINED) == 0); + assert(meow_widget_hit(bf, 100, 30) == 1); + assert(meow_widget_hit(bf, 5, 5) == 0); /* 越界 */ + meow_widget_set_enabled(bf, 0); + assert(meow_widget_hit(bf, 100, 30) == 0); /* 禁用不命中 */ + meow_widget_set_enabled(bf, 1); + meow_widget_set_selected(be, 1); + assert(meow_widget_selected(be) == 1); + printf("PASS button-variant: create/set/hit/disabled/selected\n"); + + /* ---------- TextField ---------- */ + char tfbuf[32] = ""; + meow_widget *tf = W(meow_textfield_create(10, 360, 300, 44, "Type here...", tfbuf, 32, MEOW_IME_ASCII)); + strcpy(tfbuf, "abc"); + assert(strcmp(meow_textfield_text(tf), "abc") == 0); + assert(meow_widget_hit(tf, 20, 380) == 1); + meow_textfield_set_placeholder(tf, "placeholder"); + printf("PASS textfield: create/text/placeholder/hit\n"); + + /* ---------- Card + content ---------- */ + meow_widget *card = W(meow_card_create(240, 10, 220, 180)); + meow_widget *inner = W(meow_btn_create_variant(260, 40, 180, 44, "IN CARD", MEOW_BTN_FILLED)); + meow_card_set_content(card, inner); + assert(meow_widget_hit(card, 250, 20) == 1); + printf("PASS card: create/content/hit\n"); + + /* ---------- Divider / Badge ---------- */ + meow_widget *dv = W(meow_divider_create(10, 470, 200, 0)); + meow_widget *dv2 = W(meow_divider_create(230, 470, 8, 1)); + meow_widget *bd = W(meow_badge_create(400, 100)); + meow_badge_set(bd, 3); + meow_badge_set(bd, 0); + printf("PASS divider/badge: create/set\n"); + + /* ---------- Checkbox / Radio ---------- */ + meow_widget *ck = W(meow_checkbox_create(300, 200)); + meow_widget *rd = W(meow_radio_create(300, 250)); + meow_widget_set_selected(ck, 1); + meow_widget_set_selected(rd, 1); + assert(meow_widget_selected(ck) == 1); + assert(meow_widget_hit(rd, 315, 265) == 1); + printf("PASS checkbox/radio: create/select/hit\n"); + + /* ---------- ProgressBar ---------- */ + meow_widget *pg = W(meow_progress_create(10, 520, 300, 6, 40)); + meow_progress_set(pg, 200); /* clamp 100(无 getter,仅不崩) */ + meow_widget *pgc = W(meow_progress_circular_create(420, 520, 20, 66)); + meow_progress_set(pgc, -1); /* 不定 */ + printf("PASS progress: create/set linear+circular\n"); + + /* ---------- FAB ---------- */ + meow_widget *fab = W(meow_fab_create(410, 620, 56, "+")); + assert(meow_widget_hit(fab, 430, 640) == 1); + printf("PASS fab: create/hit\n"); + + /* ---------- TopAppBar ---------- */ + meow_widget *tab_ = W(meow_topappbar_create(0, 0, 480, 56, "Title")); + meow_topappbar_set_back(tab_, 1); + int a0 = meow_topappbar_add_action(tab_, "Save"); + int a1 = meow_topappbar_add_action(tab_, "Edit"); + assert(a0 == 0 && a1 == 1); + assert(meow_topappbar_hit(tab_, 10, 28) == MEOW_TOPBAR_BACK); /* 返回区 */ + assert(meow_topappbar_hit(tab_, 440, 28) == 1); /* 右缘动作 "Edit"(最右) */ + printf("PASS topappbar: create/back/action/hit\n"); + + /* ---------- NavigationBar ---------- */ + meow_widget *nav = W(meow_navigationbar_create(0, 680, 480, 40)); + int n0 = meow_navigationbar_add_item(nav, "Home"); + int n1 = meow_navigationbar_add_item(nav, "Library"); + int n2 = meow_navigationbar_add_item(nav, "Settings"); + assert(n0 == 0 && n1 == 1 && n2 == 2); + meow_navigationbar_set_selected(nav, 2); + assert(meow_navigationbar_selected(nav) == 2); + assert(meow_navigationbar_hit(nav, 420, 700) == 2); /* 第 3 项 */ + assert(meow_navigationbar_hit(nav, 10, 700) == 0); + printf("PASS navigationbar: add/select/hit\n"); + + /* ---------- TabBar ---------- */ + meow_widget *tbb = W(meow_tabbar_create(0, 660, 480, 36)); + meow_tabbar_add_tab(tbb, "Playlist"); + meow_tabbar_add_tab(tbb, "Queue"); + meow_tabbar_set_selected(tbb, 1); + assert(meow_tabbar_hit(tbb, 300, 678) == 1); + printf("PASS tabbar: add/select/hit\n"); + + /* ---------- 列表容器(LazyColumn 虚拟化 + 滚动) ---------- */ + meow_widget *lc = W(meow_lazycolumn_create(20, 460, 260, 180, 40, 100, list_src, NULL)); + assert(meow_list_scroll(lc) == 0); + assert(meow_list_max_scroll(lc) == 100 * 40 - 180); /* 4000-180=3820 */ + meow_list_set_scroll(lc, 5000); + assert(meow_list_scroll(lc) == 3820); /* clamp 到 max */ + meow_list_scroll_by(lc, -320); + assert(meow_list_scroll(lc) == 3500); + assert(meow_list_hit_index(lc, 30, 480) == ((480 - 460 + 3500) / 40)); /* 可见项换算 */ + meow_list_set_item_h(lc, 50); + assert(meow_widget_hit(lc, 30, 470) == 1); + /* LazyRow 横向 */ + meow_widget *lr = W(meow_lazyrow_create(20, 640, 380, 40, 60, 100, list_src, NULL)); + meow_list_scroll_by(lr, 200); + assert(meow_list_hit_index(lr, 40, 660) == ((40 - 20 + 200) / 60)); + printf("PASS lazy list: scroll clamp/virtualize/hit_index (col+row)\n"); + + /* ---------- Dialog / BottomSheet 纯逻辑(create/set/add;show 阻塞故不调) ---------- */ + meow_dialog *dlg = meow_dialog_create(MEOW_DLG_SELECT, "Pick one"); + const char *items[] = { "Alpha", "Beta", "Gamma" }; + meow_dialog_set_items(dlg, items, 3, 1); + assert(meow_dialog_result(dlg) == 0); /* 未 show 默认 */ + meow_dialog_free(dlg); + meow_dialog *dc = meow_dialog_create(MEOW_DLG_CONFIRM, "Sure?"); + assert(meow_dialog_result(dc) == 0); + meow_dialog_free(dc); + meow_sheet *sh = meow_sheet_create("Options", 0); + int s0 = meow_sheet_add_item(sh, "Copy"); + int s1 = meow_sheet_add_item(sh, "Delete"); + assert(s0 == 0 && s1 == 1); + meow_sheet_free(sh); + printf("PASS dialog/sheet: create/set_items/add_item (blocking show skipped)\n"); + + /* ---------- 绘制所有控件到 back_buf(5x7 兜底),flush 出屏幕 ---------- */ + const meow_theme *th = meow_theme_get(); + meow_ui_clear(th->bg); + for (int i = 0; i < g_wn; i++) meow_widget_draw(g_w[i]); + meow_ui_flush(); + printf("PASS draw: all %d widgets rendered, screen dumps to /tmp/b2.ppm\n", g_wn); + + free_all(); + meow_ui_shutdown(); + printf("ALL PASS\n"); + return 0; +}