/* * meow_ui_widget.c — 控件层(B1.3):按钮/滑条/开关/列表最小集。 * * 控件只负责绘制与状态;命中分发由应用在 on_tap 里调 meow_widget_hit 完成 * (与 MEOW_APP_SDK.md §5 一致)。绘制读当前主题(meow_theme_get)走 back_buf。 */ #include "meow_ui.h" #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_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_LISTVIEW = 18, /* §5.5 结构化列表(行=图标+标题+副标题+右侧操作+选中) */ W_ICONBTN = 19 }; /* M7.0a 缺口#2:圆形/位图图标按钮(圆形命中 + 按压态) */ /* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */ #define MEOW_LAYOUT_MAX_CHILD 32 /* 容器(TopAppBar 动作 / NavigationBar 项 / TabBar 页签)子项上限。 */ #define MEOW_CONTAINER_MAX 8 /* TopAppBar 位图动作图标(Material Symbols Rounded 单通道 alpha,见 sb_icons.h): * 动作单元格宽(32px 图标槽水平居中 + 两侧留白 8px)与目标 ink 高(MD3 24dp 图标)。 * ink 垂直中线取栏中点(mid),与状态栏 28px 栏「文字中线」风格一致。 */ #define MEOW_TOPBAR_ACTION_W 48 #define MEOW_TOPBAR_ICON_INK 24 /* 按钮「按住」判定阈值:视觉上区分「短按按下」与「按住」(更深状态层)。 * 阈值取 meow_ui_long_press_ms()(§6.3 通用长按默认 500ms,App 可配)——与长按事件同源, * 避免「长按已触发但视觉未加深」的不一致。触发语义不变(on_tap 仍按抬手判定)。 */ /* 圆形 ProgressBar 环带厚 / 外扩边距(bgg 修复 M2.71-bug1:控件几何必须覆盖真实像素)。 * 实际像素范围 = 中心 ± (r + thk/2);margin = thk/2 + 1 保证外接框覆盖环带外缘与圆帽。 */ #define MEOW_PROGRESS_ARC_THK 4 #define MEOW_PROGRESS_ARC_MARGIN (MEOW_PROGRESS_ARC_THK / 2 + 1) /* MD3 状态层混色:c 为基色、o 为状态色、t 为 o 的占比(0-100)。 * 用于把主题 accent 向黑/白调出「按下/按住」状态层(日/夜主题各配色)。 */ static uint32_t blend_color(uint32_t c, uint32_t o, int t) { int rr = (((c >> 16) & 0xFF) * (100 - t) + ((o >> 16) & 0xFF) * t) / 100; int gg = (((c >> 8) & 0xFF) * (100 - t) + ((o >> 8) & 0xFF) * t) / 100; int bb = ((c & 0xFF) * (100 - t) + (o & 0xFF) * t) / 100; return ((uint32_t)rr << 16) | ((uint32_t)gg << 8) | (uint32_t)bb; } struct meow_widget { int type; int x, y, w, h; int owner; /* §6.1 ①:创建时绑定的 Activity;命中只在 owner==active 才生效 */ char label[128]; int pressed; /* 按钮按下视觉态(遗留字面;状态机用 MEOW_WS_PRESSED 位) */ int min, max, val; /* 滑条 */ int on; /* 开关 */ int count, sel; /* 列表 */ meow_list_source src; void *ctx; /* §5.8 batch1:统一状态机(§5.8.3)+ 通用 Options(§5.8.2)+ 自定义回调(§5.8.1)+ 布局(§5.8.5) */ unsigned state; /* MEOW_WS_* 位掩码(默认 MEOW_WS_ENABLED) */ meow_widget_opts opts; /* 通用修饰(浅拷;opts_used==0 时按控件内建默认) */ int opts_used; /* 1=set_opts 显式挂载过 */ meow_widget_render_fn render_cb; /* W_CUSTOM:自绘回调 */ meow_widget_hit_fn hit_cb; /* W_CUSTOM:自定义命中回调 */ int kind; /* W_LAYOUT:MEOW_LAYOUT_ROW/COLUMN/BOX */ 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 选中下标 */ /* W_TOPAPPBAR 动作位图图标:每项 sb_icons[] 索引(≥0=画该图标;-1=画 items[i] 文本)。 * 追加字段(calloc 归零为 0,故 create/add_action 显式置 -1 保证文本默认)。 */ int item_icon[MEOW_CONTAINER_MAX]; /* 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 */ /* W_ICONBTN 位图图标:sb_icons[] 下标(= MEOW_ICON_* 或 SB_I_*;<0 画空位图,命中照常)。 */ int icon_id; /* §5.5 虚拟化层(W_LISTVIEW 扩展:固定 item 池 + 多行渲染器 + 完全自绘 + 空态。 * calloc 归零,安全;vs_count/vs_bind 均非 NULL 才进入 pool 模式,src2 模式不受影响。) */ meow_listview_count_fn vs_count; /* 虚拟化总数回调(NULL=src2 模式) */ meow_listview_bind_fn vs_bind; /* 虚拟化 bind 回调(NULL=src2 模式) */ meow_listview_draw_fn vs_draw; /* 完全自绘回调(非 NULL 覆盖默认 renderer) */ void *vs_ctx; /* 上述回调的 ctx */ meow_lv_item *vs_pool; /* 固定 item 池(槽数 vs_pool_n) */ int vs_pool_n; /* 池容量(视口行数 + 2,静态有界) */ int *vs_bound; /* 每槽绑定的行号(-1=空闲;pool 索引 mod 映射) */ int lines; /* 行档位 1/2(默认 2=双行) */ int show_icon; /* 1=画左图标/缩略图(默认 1) */ int show_right;/* 1=画右尾件(默认 1) */ char empty_text[64]; /* 空态占位文案(首字符 0=不画) */ /* §5.5 左滑删除(列表行横向手势:滑动偏移/删除动画/回弹;calloc 归零,安全)。 * 手势由 App 在 on_touch_move 喂(swipe_begin/move/end);横向小位移判 swipe、纵向滚动优先; * 行自绘 set_item_draw 配合(M7.3 通知行)。swipe_anim 由 meow_anim 静态池驱动。 */ int swipe_enabled; /* 1=左滑删除可用(on_delete 回调有效) */ int swipe_active; /* 1=当前行处于滑动态(swipe_idx 有效) */ int swipe_idx; /* 正被滑动的行号(-1=无) */ int swipe_dx; /* 当前行横向偏移(<=0=左移;滑动/动画中) */ int swipe_drag; /* 1=手指拖动中(限行内) */ int swipe_start_x, swipe_start_y; /* 手势起点(判定方向用) */ int swipe_started; /* 1=已裁定为横向 swipe(纵向滚动手势被抑制) */ int swipe_anim_delete;/* 1=删除动画(目标 -w);0=回弹(目标 0) */ meow_listview_swipe_fn swipe_cb; /* 删除回调(idx) */ void *swipe_ctx; meow_anim *swipe_anim; /* 滑动/回弹动画(非 NULL=进行中) */ }; /* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)---- * 注意:必须先定义 struct meow_widget,才能访问其成员(C 要求类型完整)。 */ static uint32_t widget_bg(const meow_widget *w, const meow_theme *th, uint32_t def) { (void)th; return (w->opts_used && w->opts.bg) ? w->opts.bg : def; } static uint32_t widget_fg(const meow_widget *w, const meow_theme *th, uint32_t def) { (void)th; return (w->opts_used && w->opts.fg) ? w->opts.fg : def; } static int widget_radius(const meow_widget *w, int def) { return (w->opts_used && w->opts.radius >= 0) ? w->opts.radius : def; } static int widget_pady(const meow_widget *w) { return (w->opts_used && w->opts.pad_y > 0) ? w->opts.pad_y : 0; } /* 是否禁用(状态机:MEOW_WS_ENABLED 未置位)。 */ static int widget_disabled(const meow_widget *w) { return !(w->state & MEOW_WS_ENABLED); } /* MD3 状态层叠色:把基色向 onSurface 叠 t% 调出按下/按住/聚焦状态层(§5.8.4)。 */ static uint32_t state_tint(uint32_t base, const meow_theme *th, int t) { return blend_color(base, th->onSurface, t); } /* ---- TopAppBar 位图动作图标辅助(绘制与命中共用的「项宽」/「按压淡色」)---- * 项宽:位图图标用固定单元格 MEOW_TOPBAR_ACTION_W(图标槽在该格内居中);文本项沿用 * 原「文本宽 + 12」不等式(行为不变)。命中与绘制同源,避免 icon/text 混合排布错位。 */ static int topbar_item_w(const meow_widget *w, const meow_theme *th, int i) { if (w->item_icon[i] >= 0) return MEOW_TOPBAR_ACTION_W; return meow_ui_text_width(w->items[i], th->font_body) + 12; } /* 动作颜色:默认 onSurface;按压态向栏底色(surfaceContainerHigh)淡 12%(§5.8.4 状态层)。 */ static uint32_t topbar_action_color(const meow_widget *w, const meow_theme *th) { uint32_t col = th->onSurface; if (w->state & MEOW_WS_PRESSED) col = blend_color(col, th->surfaceContainerHigh, 12); return col; } /* ---- §5.8.2/§5.8.3 公共 API:Options + 状态机 ---- */ int meow_widget_set_opts(meow_widget *w, const meow_widget_opts *opts) { if (!w || !opts) return -1; w->opts = *opts; w->opts_used = 1; /* disabled/selected 立即反映到状态机位(§5.8.3)。 */ if (opts->disabled) w->state &= ~MEOW_WS_ENABLED; else w->state |= MEOW_WS_ENABLED; if (opts->selected) w->state |= MEOW_WS_SELECTED; else w->state &= ~MEOW_WS_SELECTED; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 挂载会改变绘制:标区域脏 */ return 0; } int meow_widget_opts_get(const meow_widget *w, meow_widget_opts *out) { if (!w || !out) return -1; if (!w->opts_used) { /* 默认视图:radius=-1(控件默认)、disabled/selected 取状态位。 */ memset(out, 0, sizeof *out); out->radius = -1; out->disabled = widget_disabled(w) ? 1 : 0; out->selected = (w->state & MEOW_WS_SELECTED) ? 1 : 0; return 0; } *out = w->opts; return 0; } void meow_widget_set_state(meow_widget *w, unsigned bits, int on) { if (!w) return; if (on) w->state |= bits; else w->state &= ~bits; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 状态变化会改变绘制 */ } unsigned meow_widget_state(const meow_widget *w) { return w ? w->state : MEOW_WS_ENABLED; } int meow_widget_enabled(const meow_widget *w) { return w ? (w->state & MEOW_WS_ENABLED) != 0 : 0; } void meow_widget_set_enabled(meow_widget *w, int on) { meow_widget_set_state(w, MEOW_WS_ENABLED, on ? 1 : 0); } int meow_widget_selected(const meow_widget *w) { return w ? (w->state & MEOW_WS_SELECTED) != 0 : 0; } void meow_widget_set_selected(meow_widget *w, int on) { meow_widget_set_state(w, MEOW_WS_SELECTED, on ? 1 : 0); } /* 一键叠状态层(自定义画布可选;标准控件由 SDK 自动叠,不调用):半透明状态层 + 焦点描边。 */ void meow_widget_draw_state_layer(meow_widget *w, const meow_theme *th) { if (!w || !th) return; if (widget_disabled(w)) { meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); return; /* 禁用不再叠按下/聚焦 */ } if (w->state & MEOW_WS_PRESSED) { /* 向 onSurface 叠半透明状态层铺满控件矩形(按下/按住) */ int t = (w->opts_used && w->opts.bg) ? 24 : 12; meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, t * 255 / 100); } if (w->state & MEOW_WS_FOCUSED) { int r = widget_radius(w, 10); meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); } } /* ---- 实时按压即时刷新(agent-119)---- * 按压/按住视觉由 meow_widget_draw 读实时触摸(meow_input_touch_*)呈现,但 app 只在 * on_tick(1s)/on_tap 重绘 → 快速点/按住时按压态刷新滞后/漏。此处在 SDK 触摸按下/释放/ * 按住达阈值时主动重绘命中的按钮区域并局部上屏(不等 app 重绘),即时反映按下变深/ * 释放恢复,连续快速点击每按都反映。仅针对 SDK 控件按钮(W_BTN);app 纯自绘不适用。 * 语义不变:on_tap / meow_btn_pressed / meow_widget_hit 零改动;非按钮区域不动。 */ #define MEOW_LIVE_BTN_MAX 64 static meow_widget *g_live_btns[MEOW_LIVE_BTN_MAX]; static int g_live_btn_n = 0; static meow_widget *g_live_btn = NULL; /* 实时按压中最上层按钮(无 = NULL) */ static int g_live_prev_hold = 0; /* 该按钮上次渲染是否为按住态 */ static void live_btn_register(meow_widget *w) { if (!w) return; /* M7.0a 缺口#2:IconButton 也是实时按压钮(同 meow_btn 的按压/按住即时重绘)。 */ if (w->type != W_BTN && w->type != W_ICONBTN) return; for (int i = 0; i < g_live_btn_n; i++) if (g_live_btns[i] == w) return; if (g_live_btn_n < MEOW_LIVE_BTN_MAX) g_live_btns[g_live_btn_n++] = w; } static void live_btn_unregister(meow_widget *w) { for (int i = 0; i < g_live_btn_n; i++) { if (g_live_btns[i] == w) { g_live_btns[i] = g_live_btns[--g_live_btn_n]; if (g_live_btn == w) g_live_btn = NULL; return; } } if (g_live_btn == w) g_live_btn = NULL; } /* 命中:返回坐标所在的「当前 active Activity」的最上层按钮(创建序 = 绘制序,最后者在上)。 * §6.1 ①:仅扫描 owner == 当前 active Activity 的按钮 —— 非 active Activity 的控件 * 绝不参与命中(根治 agent-119 遗留的「全屏页触摸到主页按钮位置 → 闪现 accent 底」)。 */ /* 圆形命中(IconButton;圆心=按钮中心、半径=size/2)。 */ static int iconbtn_circular_hit(const meow_widget *w, int x, int y) { int cx = w->x + w->w / 2, cy = w->y + w->h / 2; int r = (w->w < w->h ? w->w : w->h) / 2; int dx = x - cx, dy = y - cy; return (dx * dx + dy * dy) <= (r * r); } static meow_widget *live_btn_hit(int x, int y) { int active = meow_ui_activity_active(); meow_widget *r = NULL; for (int i = 0; i < g_live_btn_n; i++) { meow_widget *w = g_live_btns[i]; if (w->owner != active) continue; /* 非 active activity:跳过 */ /* M7.0a 缺口#2:IconButton 按圆形命中(角部按下不点亮圆钮);其余按矩形。 */ if (w->type == W_ICONBTN) { if (iconbtn_circular_hit(w, x, y)) r = w; } else if (x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h) { r = w; /* 越后创建越上:保留最后命中 */ } } return r; } /* 重绘并局部刷新一个按钮区。 * §6.2 收敛:若按钮已注册绘制物 → 走 meow_ui_invalidate(清底 + 按 z 重画重叠区完整 z 栈), * 避免按钮压上层 chrome 时被单控件 flush 抹掉(P1);未注册则回退单控件绘制+区域上屏。 */ static void live_btn_redraw(meow_widget *w) { if (!w) return; if (meow_draw_widget_registered(w)) { meow_ui_invalidate(w->x, w->y, w->w, w->h); return; } meow_widget_draw(w); meow_ui_flush_region(w->x, w->y, w->w, w->h); } /* 由 meow_ui_run 每帧调用(主线程):检测实时触摸状态迁移,即时重绘命中按钮的按压/按住/ * 释放,不等 app on_tick/on_tap。仅在触摸状态相对上次渲染发生变化时动作(非全屏、省性能)。 */ void meow_widget_touch_refresh(void) { int active = meow_ui_activity_active(); /* §6.1 ①:若当前按住按钮已不属于 active activity(按压期间发生了 Activity 切换, * 例如点 FULLSCREEN 后进入全屏),静默清引用——不绘制、不闪现(否则会在新页面底上 * 画出旧按钮的 accent 残留)。 */ if (g_live_btn && g_live_btn->owner != active) { g_live_btn->state &= ~MEOW_WS_PRESSED; /* §5.8.3:清除按压态(跨页面不残留) */ g_live_btn = NULL; g_live_prev_hold = 0; } int down = meow_input_touch_down(); meow_widget *hit = down ? live_btn_hit(meow_input_touch_x(), meow_input_touch_y()) : NULL; int hold = down && hit && meow_input_touch_hold_ms() >= (unsigned)meow_ui_long_press_ms(); if (!down) { /* 释放:把最近按压按钮恢复为常态 */ if (g_live_btn) { g_live_btn->state &= ~MEOW_WS_PRESSED; live_btn_redraw(g_live_btn); g_live_btn = NULL; } g_live_prev_hold = 0; return; } if (hit != g_live_btn) { /* 按下/滑至新按钮:先恢复旧(若手指滑离),再按压新 */ if (g_live_btn) { g_live_btn->state &= ~MEOW_WS_PRESSED; live_btn_redraw(g_live_btn); } g_live_btn = hit; g_live_prev_hold = hold; if (hit) { if (widget_disabled(hit)) { g_live_btn = NULL; } /* 禁用不响应按压 */ else { hit->state |= MEOW_WS_PRESSED; live_btn_redraw(hit); } } } else if (hit && hold != g_live_prev_hold) { /* 同一按钮按住达阈值:升级到更强态 */ hit->state |= MEOW_WS_PRESSED; live_btn_redraw(hit); g_live_prev_hold = hold; } } meow_widget *meow_btn_create(int x, int y, int w, int h, const char *label) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_BTN; t->x = x; t->y = y; t->w = w; t->h = h; t->owner = meow_ui_activity_active(); /* §6.1 ①:绑定当前 active Activity */ t->state = MEOW_WS_ENABLED; /* §5.8.3:默认 ENABLED */ if (label) { strncpy(t->label, label, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } live_btn_register(t); meow_draw_widget_register(t); /* §6.2 自动注册(创建即注册,App 零调用) */ return t; } meow_widget *meow_slider_create(int x, int y, int w, int min, int max, int val) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_SLIDER; t->x = x; t->y = y; t->w = w; t->h = 40; t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->min = min; t->max = (max > min) ? max : min + 1; t->val = (val < min) ? min : (val > t->max) ? t->max : val; meow_draw_widget_register(t); return t; } meow_widget *meow_switch_create(int x, int y, int on) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_SWITCH; t->x = x; t->y = y; t->w = 56; t->h = 32; t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->on = on ? 1 : 0; if (t->on) t->state |= MEOW_WS_SELECTED; /* §5.8.3:开关 on → selected */ meow_draw_widget_register(t); return t; } meow_widget *meow_list_create(int x, int y, int w, int h, int count, meow_list_source src, void *ctx) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_LIST; 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; t->src = src; t->ctx = ctx; t->sel = 0; meow_draw_widget_register(t); return t; } 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 多选位图 */ if (w->vs_pool) { free(w->vs_pool); w->vs_pool = NULL; } /* W_LISTVIEW 虚拟化池 */ if (w->vs_bound) { free(w->vs_bound); w->vs_bound = NULL; } free(w); } /* ---- §5.8.1 自定义画布:App 提供 render/hit 回调,同一个 meow_widget 基座 ---- */ meow_widget *meow_widget_custom_create(int x, int y, int w, int h, meow_widget_render_fn render_cb, meow_widget_hit_fn hit_cb, void *ctx) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_CUSTOM; t->x = x; t->y = y; t->w = w; t->h = h; t->owner = meow_ui_activity_active(); /* §6.1 ①:同既有控件绑当前 active Activity */ t->state = MEOW_WS_ENABLED; t->render_cb = render_cb; t->hit_cb = hit_cb; t->ctx = ctx; meow_draw_widget_register(t); return t; } /* ---- §5.8.5 可组合布局:Row / Column / Box + Spacer ---- */ meow_widget *meow_row_create(int x, int y, int w, int h, int spacing) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_LAYOUT; t->kind = MEOW_LAYOUT_ROW; t->x = x; t->y = y; t->w = w; t->h = h; t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->spacing = spacing > 0 ? spacing : 0; meow_draw_widget_register(t); return t; } meow_widget *meow_column_create(int x, int y, int w, int h, int spacing) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_LAYOUT; t->kind = MEOW_LAYOUT_COLUMN; t->x = x; t->y = y; t->w = w; t->h = h; t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->spacing = spacing > 0 ? spacing : 0; meow_draw_widget_register(t); return t; } meow_widget *meow_box_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_LAYOUT; t->kind = MEOW_LAYOUT_BOX; t->x = x; t->y = y; t->w = w; t->h = h; t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->spacing = 0; meow_draw_widget_register(t); return t; } meow_widget *meow_layout_spacer(int w, int h) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_SPACER; t->x = 0; t->y = 0; t->w = w > 0 ? w : 0; t->h = h > 0 ? h : 0; t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; meow_draw_widget_register(t); return t; } int meow_layout_add(meow_widget *layout, meow_widget *child) { if (!layout || layout->type != W_LAYOUT || !child) return -1; if (layout->nchildren >= MEOW_LAYOUT_MAX_CHILD) return -1; for (int i = 0; i < layout->nchildren; i++) if (layout->children[i] == child) return -1; /* 同子项去重 */ layout->children[layout->nchildren++] = child; return 0; } /* 把子控件算成绝对坐标(写进各 child 的 x,y;w/h 固定尺寸,先不做弹性)。 * Row:子项水平排(垂直居中于容器 h);Column:垂直排(水平居中);Box:叠放(各放容器左上角)。 * 子项的 margin_x/margin_y(Options)参与排布;子项为布局时递归 arrange。 */ void meow_layout_arrange(meow_widget *layout) { if (!layout || layout->type != W_LAYOUT) return; int kind = layout->kind; int cur = (kind == MEOW_LAYOUT_COLUMN) ? layout->y : layout->x; for (int i = 0; i < layout->nchildren; i++) { meow_widget *c = layout->children[i]; if (!c) continue; int mx = (c->opts_used && c->opts.margin_x > 0) ? c->opts.margin_x : 0; int my = (c->opts_used && c->opts.margin_y > 0) ? c->opts.margin_y : 0; if (kind == MEOW_LAYOUT_ROW) { cur += mx; c->x = cur; c->y = layout->y + (layout->h - c->h) / 2; /* 垂直居中 */ if (c->y < layout->y) c->y = layout->y; cur += c->w + mx + layout->spacing; } else if (kind == MEOW_LAYOUT_COLUMN) { cur += my; c->y = cur; c->x = layout->x + (layout->w - c->w) / 2; /* 水平居中 */ if (c->x < layout->x) c->x = layout->x; cur += c->h + my + layout->spacing; } else { /* BOX:叠放,各放容器左上角 */ c->x = layout->x; c->y = layout->y; } if (c->type == W_LAYOUT) meow_layout_arrange(c); /* 嵌套递归 */ } } /* ---- 布局深度反推访问器(meow_ui_draw.c 用,读本 TU 的 struct meow_widget 内部字段)---- */ int meow_widget_is_layout(const meow_widget *w) { return w && w->type == W_LAYOUT; } int meow_widget_nchild(const meow_widget *w) { return (w && w->type == W_LAYOUT) ? w->nchildren : 0; } const meow_widget *meow_widget_child_at(const meow_widget *w, int idx) { return (w && w->type == W_LAYOUT && idx >= 0 && idx < w->nchildren) ? w->children[idx] : NULL; } /* ---- 几何/归属访问器(meow_ui_draw.c 用:绘制物 rect 实时读 + owner 过滤)---- */ int meow_widget_owner(const meow_widget *w) { return w ? w->owner : 0; } void meow_widget_rect(const meow_widget *w, int out[4]) { if (!out) return; if (!w) { out[0] = out[1] = out[2] = out[3] = 0; return; } out[0] = w->x; out[1] = w->y; out[2] = w->w; out[3] = w->h; } /* 写控件矩形(§12.1 meow_box 布局引擎用:把算出的绝对坐标写进控件)。 * 只设字段,不标脏(arrange ≠ 绘制;app 在 arrange 后自行重绘 + flush_region)。 * 尺寸 clamp 到 >=0。既有的 meow_layout_arrange 在 TU 内直接访问字段,不受影响。 */ void meow_widget_set_rect(meow_widget *w, int x, int y, int wd, int ht) { if (!w) return; w->x = x; w->y = y; w->w = wd > 0 ? wd : 0; w->h = ht > 0 ? ht : 0; } /* ============================================================ */ /* §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; } /* ---- §5.8.6 M7.0a 缺口#2:IconButton(圆形/位图图标按钮)---- */ meow_widget *meow_iconbtn_create(int x, int y, int size, int icon_idx) { meow_widget *t = (meow_widget *)calloc(1, sizeof *t); if (!t) return NULL; t->type = W_ICONBTN; t->x = x; t->y = y; t->w = size; t->h = size; t->owner = meow_ui_activity_active(); /* §6.1 ①:同既有控件绑当前 active Activity */ t->state = MEOW_WS_ENABLED; t->icon_id = icon_idx; live_btn_register(t); /* 实时按压/按住即时重绘(同 meow_btn,agent-119) */ meow_draw_widget_register(t); /* §6.2 自动注册(创建即注册,App 零调用) */ return t; } /* ---- 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; meow_draw_widget_register(t); 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 文本同步到字段缓冲 + 重画字段区(invalidate 重画 z 栈相交绘制物; * flush 在 IME 阻塞循环内被挂起(meow_gfx_flush_hold),随本周期末一次原子上屏) */ 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; /* 只标脏不重画的话 back_buf 仍是旧文本,flush 上去的还是旧的——必须真重画 */ meow_ui_invalidate(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; meow_draw_widget_register(t); 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; meow_draw_widget_register(t); 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; /* 红点 */ meow_draw_widget_register(t); 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; meow_draw_widget_register(t); 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; meow_draw_widget_register(t); 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); meow_draw_widget_register(t); 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; /* bbox = 整圆像素外接框(中心 ± (r + margin)):使 w->x/y/w/h 即真实绘制范围, * 于是 meow_progress_set 的 mark_dirty 与 App 的 flush_region(w->x,w->y,w->w,w->h) * 都天然对齐,不漏刷/不多刷(修复 bgg M2.71 圆形进度脏区)。 */ int m = MEOW_PROGRESS_ARC_MARGIN; t->type = W_PROGRESS; t->x = cx - r - m; t->y = cy - r - m; t->w = 2 * (r + m); t->h = 2 * (r + m); t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->variant = 1; /* 圆形 */ t->value = value < 0 ? -1 : (value > 100 ? 100 : value); meow_draw_widget_register(t); 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; } meow_draw_widget_register(t); 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; for (int i = 0; i < MEOW_CONTAINER_MAX; i++) t->item_icon[i] = -1; /* 默认文本项 */ if (title) { strncpy(t->label, title, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } meow_draw_widget_register(t); 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->item_icon[idx] = -1; /* 文本项(后续可用 set_item_icon 转位图) */ w->nitems++; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return idx; } /* 把第 idx 个动作项设为位图图标(sb_icons[] 索引,MEOW_ICON_* 或 SB_I_*)。追加式 API: * 文本项(add_action 默认)行为不变;仅「设为图标后」该动作改用 meow_gfx_blit_icon_ink * 绘制(32px 槽居中 + ink 垂直对齐栏中点),颜色取主题 onSurface/按压态淡色。 * 返回 0=成功;NULL/非 topbar/越界 idx/非法 icon_idx=负值。 */ int meow_topappbar_set_item_icon(meow_widget *w, int idx, int icon_idx) { if (!w || w->type != W_TOPAPPBAR) return -1; if (idx < 0 || idx >= w->nitems) return -1; if (icon_idx < 0) return -1; /* ≥0 的顶层校验;越界索引由 blit 内部 clamp 兜底 */ w->item_icon[idx] = icon_idx; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; } 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 = topbar_item_w(w, th, i); /* 位图图标用固定单元格;文本项用原不等式 */ 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; meow_draw_widget_register(t); 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; meow_draw_widget_register(t); 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; } /* M2.71-bug8:选中行(越界 clamp)+ 标脏(高亮刷新)。 */ void meow_list_set_selected(meow_widget *w, int idx) { if (!w || w->type != W_LIST) return; if (idx < 0) idx = 0; if (w->count > 0 && idx >= w->count) idx = w->count - 1; if (idx == w->sel) return; w->sel = idx; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } int meow_list_selected(meow_widget *w) { return (w && w->type == W_LIST) ? w->sel : -1; } void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx) { if (!w || w->type != W_LIST) return; w->tap_cb = cb; w->tap_ctx = ctx; } /* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1(不置选中)。 */ int meow_list_tap_at(meow_widget *w, int x, int y) { int idx = meow_list_hit_index(w, x, y); if (idx < 0) return -1; meow_list_set_selected(w, idx); /* §6.4:列表成为唯一焦点——点外由 meow_ui_focus_tap 统一释放(list 焦点残留根治)。 */ meow_ui_focus_acquire((void *)w); if (w->tap_cb) w->tap_cb(idx, w->tap_ctx); 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; } /* §5.5 虚拟化辅助:当前行数(pool 模式实时查 count_fn;src2 模式用缓存的 w->count)。 */ static int listv_count(const meow_widget *w) { if (!w) return 0; if (w->vs_count) { int n = w->vs_count(w->vs_ctx); return n < 0 ? 0 : n; } return w->count; } /* 池容量估算:视口行数 + 2 静态槽(按最小行高 MEOW_LV_MIN_ITEM_H 估算,静态有界)。 * 真实列表行高远大于最小行高,故 `viewport/item_h + 1 <= cap` 恒成立 → 无槽冲突重绑。 */ #define MEOW_LV_MIN_ITEM_H 8 /* 池容量估算用的最小行高(真实列表远大于此) */ #define MEOW_LV_POOL_MAX 96 /* 池容量上限(固定;保证静态有界) */ static int listv_pool_cap(const meow_widget *w) { int cap = (w->h > 0) ? (w->h / MEOW_LV_MIN_ITEM_H + 2) : 8; if (cap < 8) cap = 8; if (cap > MEOW_LV_POOL_MAX) cap = MEOW_LV_POOL_MAX; return cap; } /* 确保池容量 ≥ cap(有界扩张;pool 模式调用)。返回 0=成功;-1=分配失败(仍可用,槽冲突时防御性重绑)。 */ static int listv_ensure_pool(meow_widget *w) { if (!w->vs_count || !w->vs_bind) return 0; int cap = listv_pool_cap(w); if (w->vs_pool && w->vs_pool_n >= cap) return 0; /* 重分配(有界:cap ≤ MEOW_LV_POOL_MAX,与 item 总数无关)。 */ meow_lv_item *np = (meow_lv_item *)calloc((size_t)cap, sizeof *np); int *nb = (int *)calloc((size_t)cap, sizeof *nb); if (!np || !nb) { free(np); free(nb); return -1; } if (w->vs_pool && w->vs_pool_n > 0) { memcpy(np, w->vs_pool, (size_t)(w->vs_pool_n < cap ? w->vs_pool_n : cap) * sizeof *np); memcpy(nb, w->vs_bound, (size_t)(w->vs_pool_n < cap ? w->vs_pool_n : cap) * sizeof *nb); } for (int i = 0; i < cap; i++) nb[i] = -1; /* 全新/重分配:全部槽空闲 → 下一帧按需绑定 */ free(w->vs_pool); free(w->vs_bound); w->vs_pool = np; w->vs_bound = nb; w->vs_pool_n = cap; return 0; } /* 取 index 对应的池槽:pool 槽按 index mod cap 映射(回滚命中同槽 → 不重绑)。 * 槽绑定的 index 不匹配 → 防御性重新 bind(保证内容正确,即使池不足)。返回槽指针。 */ static meow_lv_item *listv_slot(meow_widget *w, int index) { if (!w->vs_pool || w->vs_pool_n <= 0) return NULL; int s = (int)((unsigned)index % (unsigned)w->vs_pool_n); if (w->vs_bound[s] != index) { memset(&w->vs_pool[s], 0, sizeof w->vs_pool[s]); w->vs_bind(index, &w->vs_pool[s], w->vs_ctx); w->vs_bound[s] = index; } return &w->vs_pool[s]; } /* 文本截断:超出 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->swipe_idx = -1; t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE; t->lines = 2; t->show_icon = 1; t->show_right = 1; /* 默认双行富 item + 图标 + 右尾件(src2 布局不变) */ 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; listv_ensure_pool(w); /* 行高变小 → 可见行增多 → 池容量随之调整 */ 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; int n = listv_count(w); /* pool 模式忽略 set_count(count 实时查) */ if (w->sel >= n) w->sel = -1; if (w->sel_mode == MEOW_LIST_SEL_MULTI && !w->vs_count) { /* 重分配多选位图(src2 模式) */ free(w->sel_bits); w->sel_bits = NULL; w->sel_bits_n = 0; int need = (n + 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; long long ms = (long long)listv_count(w) * listv_item_h(w) - w->h; if (ms < 0) return 0; return ms > 0x7FFFFFFF ? 0x7FFFFFFF : (int)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; int n = listv_count(w); if (n <= 0) { w->sel = -1; return; } if (idx >= n) idx = n - 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, total = listv_count(w); if (w->sel_mode == MEOW_LIST_SEL_MULTI) { for (int i = 0; i < total && 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, total = listv_count(w); for (int i = 0; i < total; 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; int n = listv_count(w); if (idx < 0 || idx >= n) 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; } else if (w->vs_bind) { /* pool 模式:bind 临时查 enabled */ meow_lv_item it; memset(&it, 0, sizeof it); w->vs_bind(idx, &it, w->vs_ctx); 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; } else if (w->vs_bind) { meow_lv_item it; memset(&it, 0, sizeof it); w->vs_bind(idx, &it, w->vs_ctx); 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); meow_ui_focus_acquire((void *)w); /* §6.4:列表成为唯一焦点(点外由 focus_tap 释放) */ if (w->lv_tap_cb) w->lv_tap_cb(idx, op, w->lv_tap_ctx); return idx; } /* ============================================================ */ /* §5.5 虚拟化层 API(W_LISTVIEW 扩展;pool 模式;src2 模式不变) */ /* ============================================================ */ int meow_listview_set_vsource(meow_widget *w, meow_listview_count_fn count, meow_listview_bind_fn bind, void *ctx) { if (!w || w->type != W_LISTVIEW || !count || !bind) return -1; w->vs_count = count; w->vs_bind = bind; w->vs_ctx = ctx; w->vs_draw = NULL; /* 重挂源:清自绘(默认 renderer) */ if (listv_ensure_pool(w) != 0) { /* 分配固定 item 池(有界) */ w->vs_count = NULL; w->vs_bind = NULL; /* 池分配失败:回退 src2 模式 */ return -1; } meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; } int meow_listview_set_item_renderer(meow_widget *w, int lines, int show_icon, int show_right) { if (!w || w->type != W_LISTVIEW) return -1; if (lines < 1) lines = 1; if (lines > 2) lines = 2; w->lines = lines; w->show_icon = show_icon ? 1 : 0; w->show_right = show_right ? 1 : 0; if (w->item_h <= 0) w->item_h = (lines == 1) ? 48 : 56; /* 未显式定行高 → 档位默认 */ listv_ensure_pool(w); /* 行档位可能变化 → 确保池容量 */ meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; } int meow_listview_set_item_tier(meow_widget *w, int lines) { if (!w || w->type != W_LISTVIEW) return -1; if (lines < 1) lines = 1; if (lines > 2) lines = 2; w->lines = lines; w->item_h = (lines == 1) ? 48 : 56; /* 档位默认高(可随后 set_item_h 覆盖) */ listv_ensure_pool(w); meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; } int meow_listview_set_item_draw(meow_widget *w, meow_listview_draw_fn draw, void *ctx) { if (!w || w->type != W_LISTVIEW) return -1; w->vs_draw = draw; w->vs_ctx = ctx; /* 自绘覆盖默认 renderer;NULL=用默认 */ meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; } int meow_listview_set_empty_text(meow_widget *w, const char *text) { if (!w || w->type != W_LISTVIEW) return -1; if (text) { strncpy(w->empty_text, text, sizeof w->empty_text - 1); w->empty_text[sizeof w->empty_text - 1] = '\0'; } else { w->empty_text[0] = '\0'; } meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; } /* ============================================================ */ /* §5.5 左滑删除(M7.3 缺口 #9):手势判定 + 删除/回弹动画 */ /* ============================================================ */ #define MEOW_SWIPE_SLOP 10 /* 手势判定容差(px):超过才裁定方向 */ /* 删除阈值:|dx| ≥ 行宽*2/5。 */ static int swipe_thresh(const meow_widget *w) { return w->w * 2 / 5; } /* 动画更新:设行偏移 + 重绘列表(只画可见行;widget clip 已裁剪)。 */ static void swipe_anim_update(meow_anim *a, float v, void *ctx) { (void)a; meow_widget *lv = (meow_widget *)ctx; if (!lv) return; lv->swipe_dx = (int)v; meow_widget_draw(lv); } static void swipe_anim_done(meow_anim *a, int canceled, void *ctx) { (void)a; meow_widget *lv = (meow_widget *)ctx; if (!lv) return; int del = lv->swipe_anim_delete; int idx = lv->swipe_idx; lv->swipe_anim = NULL; lv->swipe_anim_delete = 0; lv->swipe_active = 0; lv->swipe_idx = -1; lv->swipe_dx = 0; if (canceled) return; /* 取消:回正常位(无回调) */ if (del && lv->swipe_cb) /* 删除:滑出完成 → 回调(App 移除行 + 刷新 count) */ lv->swipe_cb(idx, lv->swipe_ctx); } void meow_listview_set_swipe_delete(meow_widget *lv, int enabled, meow_listview_swipe_fn on_delete, void *ctx) { if (!lv || lv->type != W_LISTVIEW) return; if (lv->swipe_anim) { meow_anim_cancel(lv->swipe_anim); lv->swipe_anim = NULL; } lv->swipe_enabled = enabled ? 1 : 0; lv->swipe_cb = on_delete; lv->swipe_ctx = ctx; lv->swipe_active = 0; lv->swipe_idx = -1; lv->swipe_dx = 0; lv->swipe_drag = 0; lv->swipe_started = 0; } int meow_listview_swipe_begin(meow_widget *lv, int x, int y) { if (!lv || lv->type != W_LISTVIEW || !lv->swipe_enabled) return 0; int idx = meow_listview_hit(lv, x, y); if (idx < 0) return 0; if (lv->swipe_anim) return 1; /* 已有动画:吞掉,不干扰当前行 */ lv->swipe_drag = 1; lv->swipe_started = 0; lv->swipe_idx = idx; lv->swipe_dx = 0; lv->swipe_start_x = x; lv->swipe_start_y = y; return 1; } int meow_listview_swipe_move(meow_widget *lv, int x, int y) { if (!lv || lv->type != W_LISTVIEW || !lv->swipe_drag) return 0; int dx = x - lv->swipe_start_x, dy = y - lv->swipe_start_y; int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; if (!lv->swipe_started) { if (ady > adx && ady > MEOW_SWIPE_SLOP) { /* 纵向优先:取消 swipe,让位滚动 */ lv->swipe_drag = 0; lv->swipe_active = 0; lv->swipe_idx = -1; lv->swipe_dx = 0; return 0; } if (adx > MEOW_SWIPE_SLOP && adx > ady) { /* 横向小位移:裁定为 swipe */ lv->swipe_started = 1; lv->swipe_active = 1; } else { return 0; /* 太小未裁定:暂不消费 */ } } else { if (ady > adx && ady > MEOW_SWIPE_SLOP) { /* 中途纵向接管:取消,让位滚动 */ lv->swipe_drag = 0; lv->swipe_active = 0; lv->swipe_idx = -1; lv->swipe_dx = 0; return 0; } } /* 只左滑(dx<0)露右删除区;右滑 clamp 到 0;触底 clamp 到 -w。 */ int off = dx < 0 ? dx : 0; if (off < -lv->w) off = -lv->w; if (off == lv->swipe_dx) return 1; lv->swipe_dx = off; meow_gfx_mark_dirty(lv->x, lv->y, lv->w, lv->h); return 1; } int meow_listview_swipe_end(meow_widget *lv, int x, int y) { (void)x; (void)y; if (!lv || lv->type != W_LISTVIEW) return 0; if (!lv->swipe_drag) return 0; /* 非 swipe 行(tap/滚动)→ App 自理 */ lv->swipe_drag = 0; if (!lv->swipe_started) return 0; /* 无位移 = 点击 → App 走 tap_at */ int dx = lv->swipe_dx; int del = (-dx >= swipe_thresh(lv)); /* 超阈值 → 删除;否则回弹 */ int item = listv_item_h(lv); int ry = lv->y + ((unsigned)lv->swipe_idx * (unsigned)item - lv->scroll); if (lv->swipe_anim) { meow_anim_cancel(lv->swipe_anim); lv->swipe_anim = NULL; } lv->swipe_anim_delete = del; meow_anim_desc d; memset(&d, 0, sizeof d); d.from = (float)dx; d.to = (float)(del ? -lv->w : 0); d.duration_ms = del ? 160u : 140u; d.ease = del ? MEOW_EASE_OUT : MEOW_EASE_OVERSHOOT; d.update = swipe_anim_update; d.done = swipe_anim_done; d.ctx = lv; meow_anim *an = meow_anim_start(&d); if (!an) { /* 池满:直接终态(防卡死) */ if (del) { int i = lv->swipe_idx; lv->swipe_idx = -1; lv->swipe_dx = 0; if (lv->swipe_cb) lv->swipe_cb(i, lv->swipe_ctx); } lv->swipe_active = 0; lv->swipe_dx = 0; meow_gfx_mark_dirty(lv->x, lv->y, lv->w, lv->h); return 1; } lv->swipe_anim = an; meow_anim_set_region(an, lv->x, ry < lv->y ? lv->y : ry, lv->w, item); /* 动画帧只刷该行带 */ return 1; } int meow_listview_swipe_active(meow_widget *lv) { return (lv && lv->type == W_LISTVIEW && (lv->swipe_active || lv->swipe_anim)) ? 1 : 0; } int meow_listview_swipe_offset(meow_widget *lv) { return (lv && lv->type == W_LISTVIEW) ? lv->swipe_dx : 0; } static int hit(meow_widget *w, int x, int y) { if (!w) return 0; return x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h; } int meow_widget_hit(meow_widget *w, int x, int y) { if (!w) return 0; /* §6.1 ①:页面控件只对自身 Activity 负责,不跨页面复用命中。 */ if (w->owner != meow_ui_activity_active()) return 0; /* §5.8.3:禁用不响应命中(也不进 pressed)。 */ if (widget_disabled(w)) return 0; /* §5.8.1:自定义画布用 App 回调判定命中(可非矩形);IconButton 圆形命中;其余矩形内含。 */ if (w->type == W_CUSTOM) return w->hit_cb ? w->hit_cb(w, x, y, w->ctx) : hit(w, x, y); if (w->type == W_ICONBTN) return iconbtn_circular_hit(w, x, y); return hit(w, x, y); } int meow_btn_pressed(meow_widget *w) { return w && w->type == W_BTN ? (w->pressed || (w->state & MEOW_WS_PRESSED)) : 0; } void meow_slider_set(meow_widget *w, int val) { if (!w || w->type != W_SLIDER) return; if (val < w->min) val = w->min; if (val > w->max) val = w->max; if (val == w->val) return; w->val = val; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 滑块/轨道随值变化:标脏 */ } int meow_slider_get(meow_widget *w) { return w && w->type == W_SLIDER ? w->val : 0; } void meow_switch_set(meow_widget *w, int on) { if (w && w->type == W_SWITCH) { w->on = on ? 1 : 0; if (w->on) w->state |= MEOW_WS_SELECTED; /* §5.8.3:on → selected */ else w->state &= ~MEOW_WS_SELECTED; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } } int meow_switch_get(meow_widget *w) { return w && w->type == W_SWITCH ? w->on : 0; } /* 供应用在 on_tap 里对外可控回馈:这里不存命中态(由 app 分发),仅暴露绘制。 * §5.8 升级:统一取语义色(meow_theme),读 opts(背景/圆角/描边/pad/fg)与状态机位 * (disabled/selected/pressed/focused),标准控件由 SDK 自动叠状态层;自定义画布(W_CUSTOM) * 由 App render_cb 全权自绘(SDK 只注入主题、不画背景/状态层);布局(W_LAYOUT)递归绘制子控件。 */ void meow_widget_draw(meow_widget *w) { if (!w) return; /* 绘制期裁剪(§3):把绘制 clip 设为该控件矩形,画完恢复——任何控件(含 W_CUSTOM * 自定义画布)不可能画出自身边界,从根上杜绝越界脏区。可嵌套(W_LAYOUT/W_CARD 递归 * 子控件会再 push 子控件矩形);顶层 clip=全屏,故 IME/模态直绘不受影响。 */ meow_gfx_clip_push(w->x, w->y, w->w, w->h); const meow_theme *th = meow_theme_get(); int disabled = widget_disabled(w); switch (w->type) { case W_BTN: { /* 按下/按住 = 视觉态(不改 on_tap/btn_pressed/hit 语义)。按下来源三路: * - 状态机 MEOW_WS_PRESSED(App 经 set_state 或 SDK 触摸层置位); * - w->pressed(既有字面,保留); * - 实时触摸命中:SDK 输入层当前正按在该控件内(无需 app 追踪)。 * 按住=实时触摸命中且持续 ≥MEOW_BTN_HOLD_MS;按下=任一来源。禁用时不叠按下底。 */ int live = meow_input_touch_down() && meow_input_touch_x() >= w->x && meow_input_touch_x() < w->x + w->w && meow_input_touch_y() >= w->y && meow_input_touch_y() < w->y + w->h; int press = (w->state & MEOW_WS_PRESSED) || w->pressed || live; int hold = live && meow_input_touch_hold_ms() >= (unsigned)meow_ui_long_press_ms(); if (disabled) press = 0; /* §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); 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 (fill) bg = (hold) ? state_tint(base, th, 24) : (press ? state_tint(base, th, 12) : base); else bg = 0; int r = widget_radius(w, 10); 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 = default_fg; meow_ui_text_center(cx, cy, w->label, th->font_body, tc); /* 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); /* 禁用:整体变暗(圆角 α,防在圆角外叠出灰方块——M2.71-bug4) */ if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, th->onSurface, 90); break; } case W_ICONBTN: { /* M7.0a 缺口#2:圆形底 + 位图图标 + 按压/按住状态层(实时)。 * 按压三路来源(同 meow_btn,agent-119):状态机 MEOW_WS_PRESSED / 实时触摸命中 / * 位图取自 sb_icons[](meow_gfx_blit_icon_ink,只写像素不标顶栏脏区——控件矩形由 * 本 draw 的 clip + invalidation 管理);图标墨色 onSurface,选中/按住淡向底。 */ int live = meow_input_touch_down() && iconbtn_circular_hit(w, meow_input_touch_x(), meow_input_touch_y()); int press = (w->state & MEOW_WS_PRESSED) || live; int hold = live && meow_input_touch_hold_ms() >= (unsigned)meow_ui_long_press_ms(); int disabled = widget_disabled(w); if (disabled) press = 0; int cx = w->x + w->w / 2, cy = w->y + w->h / 2; int r = (w->w < w->h ? w->w : w->h) / 2; int inkh = (w->h * 6) / 10; /* MD3 图标比:约 24/40 → 0.6*size */ if (inkh < 8) inkh = (w->h > 0) ? w->h : 8; int selected = (w->state & MEOW_WS_SELECTED); uint32_t base = selected ? th->primaryContainer : widget_bg(w, th, th->surfaceContainerLow); uint32_t fgc = selected ? th->onPrimaryContainer : widget_fg(w, th, th->onSurface); uint32_t bg = (hold) ? state_tint(base, th, 24) : (press ? state_tint(base, th, 12) : base); if (disabled) bg = state_tint(base, th, 38); /* 禁用变淡(圆形底直接变灰) */ if (w->state & MEOW_WS_FOCUSED) bg = state_tint(base, th, 8); /* 焦点浅提亮 */ meow_gfx_fill_circle(cx, cy, r, bg); if (w->icon_id >= 0) meow_gfx_blit_icon_ink(w->icon_id, cx - MEOW_SB_ICON_SIZE / 2, inkh, fgc, cy); if (w->state & MEOW_WS_FOCUSED) meow_gfx_ring_arc(cx, cy, r - 1, 2, 0, 360, th->primary); /* 焦点圆形描边 */ break; } case W_SLIDER: { /* 语义色:轨道=onSurfaceVariant(dim)、填充=primary(accent)、滑块=fg(onSurface)。 */ int trace_y = w->y + w->h / 2 - 3; meow_ui_fill_rect(w->x, trace_y, w->w, 6, th->onSurfaceVariant); int range = w->max - w->min; int frac = range ? (w->val - w->min) * w->w / range : 0; if (frac > 0) meow_ui_fill_rect(w->x, trace_y, frac, 6, th->primary); int tx = w->x + frac; int ty = w->y + w->h / 2; meow_gfx_fill_round(tx - 10, ty - 10, 20, 20, 10, th->onSurface); if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; } case W_SWITCH: { uint32_t bg = w->on ? th->primary : th->onSurfaceVariant; meow_gfx_fill_round(w->x, w->y, w->w, w->h, w->h / 2, bg); int knob_r = w->h / 2 - 3; int kx = w->on ? w->x + w->w - knob_r * 2 - 3 : w->x + 3; meow_gfx_fill_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, 0xFFFFFF); if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->h / 2, th->onSurface, 90); break; } case W_LIST: { /* §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; /* 视口绝对边界:列表只在其内绘制,杜绝越界泄漏(部分行滚出视口时把填充/分隔线/文本 * 画到视口外 → 标脏并上屏 → 标题栏/底部残留脏区,P3)。 */ int vx0 = w->x, vy0 = w->y, vx1 = w->x + w->w, vy1 = w->y + w->h; int thh = meow_ui_text_height(th->font_body); for (int i = first; i < last; i++) { char label[64] = ""; 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 >= vx1) break; /* 完全在右缘外 */ if (rx + item <= vx0) continue; /* 完全在左缘外(滚出残余) */ int cl = rx > vx0 ? rx : vx0; /* 可见左界 */ int cr = (rx + item) < vx1 ? (rx + item) : vx1; /* 可见右界 */ if (i == w->sel) meow_ui_fill_rect(cl, w->y, cr - cl, w->h, th->primary); uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface; int ty = w->y + (w->h - thh) / 2; if (rx >= vx0 && rx + item <= vx1) /* 仅整项可见才画文本(防泄漏) */ meow_ui_text_center(rx + item / 2, ty, label, th->font_body, col); int sxp = rx + item - 1; /* 分隔线:仅当落在可见区 */ if (sxp >= cl && sxp < cr) meow_ui_fill_rect(sxp, w->y, 1, w->h, th->onSurfaceVariant); } else { int ry = w->y + pos; if (ry >= vy1) break; /* 完全在底缘外 */ if (ry + item <= vy0) continue; /* 完全在顶缘外(滚出残余) */ int cl = ry > vy0 ? ry : vy0; /* 可见顶界 */ int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ if (i == w->sel) meow_ui_fill_rect(w->x, cl, w->w, cr - cl, th->primary); uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface; int ty = ry + (item - thh) / 2; if (ty >= vy0 && ty + thh <= vy1) /* 文本不越界才画(防标题/底部泄漏) */ meow_ui_text(w->x + pad, ty, label, th->font_body, col); int sy = ry + item - 1; /* 分隔线:仅当落在可见区 */ if (sy >= cl && sy < cr) meow_ui_fill_rect(w->x, sy, w->w, 1, th->onSurfaceVariant); } } if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; } case W_LISTVIEW: { /* §5.5 结构化列表(纵向):只画可见行 + 视口裁剪(M2.73)。三路: * ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变; * ② 虚拟化 pool 模式(set_vsource + set_item_renderer):固定 item 池(进入视口才 bind), * 默认 renderer 画多行富 item(图标+标题+副标题+右尾件),行高可配;滚动只移 viewport; * ③ 完全自绘(set_item_draw):App 自绘每可见行(SDK 只给矩形+状态位)。 * 空态:count==0 且设 empty_text → 视口内居中。全语义色;选中/按压/禁用态走主题。 */ int item = listv_item_h(w); int vp = w->h; /* 纵向 */ int off = w->scroll; int count = listv_count(w); int maxs = (int)((long long)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 > count) last = 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 + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */ int op_start = (w->show_right) ? (vx0 + (w->w * 2 / 3)) /* 行右 1/3 = 操作区 */ : (vx0 + w->w - pad); int text_maxw = (w->show_right) ? (op_start - pad - text_x) /* 标题/副标题可用宽(不压操作区) */ : ((vx0 + w->w - pad) - text_x); if (text_maxw < 8) text_maxw = 8; /* 空态占位(§5.5):count==0 且设了 empty_text → 视口内居中画文案。 */ if (count == 0 && w->empty_text[0]) { int ty = vy0 + (w->h - thh) / 2; meow_ui_text_center(vx0 + w->w / 2, ty, w->empty_text, th->font_body, th->onSurfaceVariant); if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; } if (w->vs_count && w->vs_bind) { /* 虚拟化 pool 模式 */ listv_ensure_pool(w); /* 防御:视口/行高变化时确保池容量(cap 已够则零开销保留绑定) */ if (w->vs_draw) { /* ③ 完全自绘:App 画整行(widget clip 已裁到列表视口) */ for (int i = first; i < last; i++) { int ry = w->y + (i * item - off); if (ry >= vy1) break; int st = 0; if (i == w->sel && w->sel_mode != MEOW_LIST_SEL_NONE) st |= MEOW_LISTVIEW_ST_SELECTED; if (meow_input_touch_down() && meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w && meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item) st |= MEOW_LISTVIEW_ST_PRESSED; int rx0 = vx0; /* 左滑行:行 x 左移 + 右露删除区 */ if (w->swipe_active && w->swipe_idx == i && w->swipe_dx < 0) { meow_ui_fill_rect(vx0 + w->w + w->swipe_dx, ry, -w->swipe_dx, item, th->error); int thh2 = meow_ui_text_height(th->font_body); int dly = ry + (item - thh2) / 2; if (dly >= vy0 && dly + thh2 <= vy1) meow_ui_text_center(vx0 + w->w + w->swipe_dx + (-w->swipe_dx) / 2, dly, meow_str("common.delete"), th->font_body, th->onError); rx0 = vx0 + w->swipe_dx; } w->vs_draw(i, rx0, ry, w->w, item, st, w->vs_ctx); } } else { /* ② 默认 renderer:固定 item 池(进入视口才 bind) */ for (int i = first; i < last; i++) { meow_lv_item *it = listv_slot(w, i); /* 绑定到固定池槽(回滚同槽不重绑) */ if (!it) 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 && w->sel_mode != MEOW_LIST_SEL_NONE); int press = meow_input_touch_down() && meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w && meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item; /* 左滑删除(M7.3):本行被滑动 → 右露删除区 + 行 x 左移。非滑动行 shift=0 → 输出不变。 */ int shift = 0; if (w->swipe_active && w->swipe_idx == i && w->swipe_dx < 0) { meow_ui_fill_rect(vx0 + w->w + w->swipe_dx, ry, -w->swipe_dx, item, th->error); int thh2 = meow_ui_text_height(th->font_body); int dly = ry + (item - thh2) / 2; if (dly >= vy0 && dly + thh2 <= vy1) meow_ui_text_center(vx0 + w->w + w->swipe_dx + (-w->swipe_dx) / 2, dly, meow_str("common.delete"), th->font_body, th->onError); shift = w->swipe_dx; } int rlx = vx0 + shift; /* 行内容左移后的 x */ int rtx = text_x + shift; /* 文本 x(随行左移) */ int rop = op_start + shift; /* 操作区分界 x */ if (it->section) { /* 段头占位行(非粘性):surfaceContainerHigh 底 + primary 段名 */ meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->surfaceContainerHigh); int sty = ry + (item - thh) / 2; if (it->title && sty >= vy0 && sty + thh <= vy1) listv_text_trunc(rtx, sty, it->title, text_maxw, th->font_body, th->primary); int sy = ry + item - 1; if (sy >= cl && sy < cr) meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant); if (!it->enabled) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90); continue; } /* 选中/按压背景(primaryContainer / 半透明主题色按压层) */ if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer); else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26); uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; /* 左图标/缩略图(show_icon=1;越界不画防泄漏) */ if (w->show_icon) { 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, rlx + pad, icy, it->icon_kind, it->icon, iccol); } /* 标题 + 副标题(双行居中;单行(lines==1 或无副标题)单行居中);越界不画 */ if (w->lines == 2 && it->subtitle && it->subtitle[0]) { int ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; if (it->title && ty >= vy0 && ty + thh <= vy1) listv_text_trunc(rtx, 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(rtx, sy, it->subtitle, text_maxw, th->font_body, subcol); } else { int ty = ry + (item - thh) / 2; if (it->title && ty >= vy0 && ty + thh <= vy1) listv_text_trunc(rtx, ty, it->title, text_maxw, th->font_body, textcol); } /* 右尾件(show_right=1:箭头/开关/复选/⋯) */ if (w->show_right && it->right != MEOW_LIST_RIGHT_NONE) { int ax = rlx + w->w - pad - 24; /* 右缘动作槽(约 24px) */ if (ax >= rop) 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(rlx, sy, w->w, 1, th->outlineVariant); /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ if (!it->enabled) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90); } } } else { /* ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变。 * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 */ 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; /* 左滑删除(M7.3):本行被滑动 → 右露删除区 + 行 x 左移。非滑动行 shift=0 → 输出不变。 */ int shift = 0, rlx = vx0, rtx = text_x, rop = op_start; if (w->swipe_active && w->swipe_idx == i && w->swipe_dx < 0) { meow_ui_fill_rect(vx0 + w->w + w->swipe_dx, ry, -w->swipe_dx, item, th->error); int thh2 = meow_ui_text_height(th->font_body); int dly = ry + (item - thh2) / 2; if (dly >= vy0 && dly + thh2 <= vy1) meow_ui_text_center(vx0 + w->w + w->swipe_dx + (-w->swipe_dx) / 2, dly, meow_str("common.delete"), th->font_body, th->onError); shift = w->swipe_dx; rlx = vx0 + shift; rtx = text_x + shift; rop = op_start + shift; } if (sel) meow_ui_fill_rect(rlx, 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, rlx + 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(rtx, 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(rtx, 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(rtx, ty, it.title, text_maxw, th->font_body, textcol); } /* 右侧操作区 */ if (it.right != MEOW_LIST_RIGHT_NONE) { int ax = rlx + w->w - pad - 24; /* 右缘动作槽(约 24px) */ if (ax >= rop) 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(rlx, sy, w->w, 1, th->outlineVariant); /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ if (!it.enabled) meow_ui_fill_rect_alpha(rlx, 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(通用修饰)自行叠。 */ if (w->render_cb) w->render_cb(w, th, w->ctx); break; case W_LAYOUT: /* 递归绘制子控件(子控件坐标已由 meow_layout_arrange 算成绝对坐标)。 */ 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_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, 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_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, 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_gfx_fill_round_alpha(w->x, w->y, sz, sz, r, 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_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->w / 2, th->onSurface, 90); break; } case W_PROGRESS: { if (w->variant == 1) { /* 圆形(几何 = 整圆像素外接框;由此解回圆心/半径) */ int thk = MEOW_PROGRESS_ARC_THK; int m = MEOW_PROGRESS_ARC_MARGIN; int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2; int rr = w->w / 2 - m; /* 与 create 一致:回到调用方传入的半径 r */ if (rr <= 0) break; 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_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, 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); /* 动作区:从右往左排。位图图标(item_icon>=0)用 sb_icons[] 位图(Material Symbols * Rounded)经 meow_gfx_blit_icon_ink 画(32px 槽在动作格内水平居中 + ink 垂直对齐 mid, * 颜色 = onSurface/按压淡色);文本项沿用 meow_ui_text_center(行为不变)。 */ int ay = mid - meow_ui_text_height(th->font_body) / 2; uint32_t acol = topbar_action_color(w, th); int ax = w->x + w->w - 12; for (int i = w->nitems - 1; i >= 0; i--) { int aw = topbar_item_w(w, th, i); ax -= aw; if (w->item_icon[i] >= 0) { int slot_x = ax + (aw - MEOW_SB_ICON_SIZE) / 2; /* 32px 槽在格内居中 */ meow_gfx_blit_icon_ink(w->item_icon[i], slot_x, MEOW_TOPBAR_ICON_INK, acol, mid); } else { meow_ui_text_center(ax + aw / 2, ay, w->items[i], th->font_body, acol); } } 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: break; } meow_gfx_clip_pop(); /* 恢复进入前的 clip(嵌套:回到父控件矩形/全屏) */ }