From a70cdbc0f7636eb861618137f439d96307de9f36 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 3 Sep 2026 05:59:59 +0800 Subject: [PATCH] =?UTF-8?q?sdk:=20=E8=A1=A5=E5=90=8C=E6=AD=A5=20swipe-dele?= =?UTF-8?q?te=20=E7=BC=BA=E5=A4=B1=E6=96=87=E4=BB=B6(widget/widgets.h/inte?= =?UTF-8?q?rnal.h/build=5Fsdk.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + sdk/include/meow_ui_widgets.h | 30 ++++ sdk/meow_ui_widget.c | 292 ++++++++++++++++++++++++++++++---- 3 files changed, 295 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a6a3a03..b68aa22 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ cd ../examples/hello - **滚动物理 `meow_scroll`**(§12.3):速度采样 + 松手 fling + 边界 clamp/简单回弹 + snap 吸附;给 `meow_list`/`W_LIST`/`W_LISTVIEW` 加惯性滚动。 - **事件分发形式化**(§6.4):单一仲裁者——z 序自顶向下命中、命中即消费、down 捕获整个手势、模态层隔离、唯一焦点(点外释放/滚动容器转移)。 - **ListView 虚拟化**(§5.5):固定 item 池(视口+2)、多行富 item、段头、空态、完全自绘钩子;App 只提供「总数 + bind」回调,几千/上万行不卡。 +- **ListView 左滑删除**(§5.5):列表行横向手势 `meow_listview_swipe_begin/move/end` 三级喂入 + 删除/回弹动画;`meow_listview_set_swipe_delete` 绑删除回调,行自绘 `set_item_draw` 配合画右露删除区(主题 `error/onError`)。 +- **圆形图标按钮 `meow_iconbtn_create`**(M7.0a):圆形命中 + 位图图标(`sb_icons[]`/`MEOW_ICON_*`)+ 实时按压/按住态;供电源菜单/播放控制/锁屏件用。 - **字符串资源 / i18n**(§13):`meow_str`/`meow_strf`/`meow_str_set_locale`——内嵌 `zh_cn`/`en_us` 双语表 + TF 卡覆盖表;UI 文案零硬编码,按 locale 取整串。 - **图标库 `sb_icons`**(§5.8):30 个 MD3 Material Symbols 位图动作图标(`MEOW_ICON_*`),`meow_topappbar_set_item_icon` 把 TopAppBar 动作项换成位图图标(修复 ⚙↻⌕ 豆腐)。 - **图片降采样 `meow_img_downscale`**(`include/meow_ui_image.h`):把解码图降到 ≤maxdim 边长省内存(封面/大图用,如 8.28MB→0.78MB)。 diff --git a/sdk/include/meow_ui_widgets.h b/sdk/include/meow_ui_widgets.h index c43824b..3ebf369 100644 --- a/sdk/include/meow_ui_widgets.h +++ b/sdk/include/meow_ui_widgets.h @@ -210,6 +210,17 @@ enum { MEOW_ICON_HEART = 29, }; +/* ---- IconButton(圆形/位图图标按钮,M7.0a 缺口#2)---- + * 圆形命中 + 位图图标(sb_icons[],Material Symbols Rounded 单通道 alpha)+ 按压态(实时)。 + * 用途:电源菜单圆钮 / Now Playing 播放控制 / 锁屏常用件等「位图图标 + 圆形」按钮。 + * icon_idx = sb_icons[] 下标(用上面 MEOW_ICON_* 枚举或 SB_I_*;越界画空位图,命中照常)。 + * size = 按钮直径(外接正方形 w=h=size;图标 ink 高 = size*6/10,MD3 图标比)。 + * 语义色:底 surfaceContainerLow、图标 onSurface;按压叠 onSurface 状态层(实时,同 meow_btn); + * 禁用变淡;选中态 primaryContainer/onPrimaryContainer(meow_widget_set_selected)。 + * 命中:圆形(半径=size/2,圆心=按钮中心),owner==active + enabled 双闸门。 + * 返回按钮句柄;App 在 on_tap 里用 meow_widget_hit 判定是否命中本钮(命中即触发动作)。 */ +meow_widget *meow_iconbtn_create(int x, int y, int size, int icon_idx); + /* ---- 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); @@ -375,6 +386,25 @@ int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw, /* 空态占位:count==0 时视口内居中画 empty_text(NULL 不画)。返回 0=成功。 */ int meow_listview_set_empty_text(meow_widget *lv, const char *text); +/* ============================================================ */ +/* §5.5 左滑删除(M7.3 通知中心缺口 #9):列表行横向手势 + 删除/回弹动画 */ +/* ------------------------------------------------------------ + * 行自绘 set_item_draw 配合(SDK 在自绘行前把该行横向偏移 swipe_dx 传给 x,并画右露删除区)。 + * 手势由 App 在 on_touch_move 喂三级:begin(touch-down)/ move(touch-move)/ end(release)。 + * 判定:横向小位移|dx|>|dy| 且超容差=swipe(move 返回 1=本列表消费,App 勿滚动);纵向 + * |dy|>|dx| 且超容差=滚动手势优先(返回 0 并取消本行 swipe,App 照常 scroll_by);点按 + * 无位移(end 时未 started)=行点击(返回 0,App 照常 tap_at)。不修改 tap_at/滚动语义。 + * 动画:超阈值 → 删除动画(行滑出 meow_anim,结束后回调 on_delete(idx));未超 → 回弹。 + * 颜色:删除区用主题 error/onError;不与行点击/滚动冲突(见判定表)。 */ +typedef void (*meow_listview_swipe_fn)(int idx, void *ctx); +void meow_listview_set_swipe_delete(meow_widget *lv, int enabled, + meow_listview_swipe_fn on_delete, void *ctx); +int meow_listview_swipe_begin(meow_widget *lv, int x, int y); /* touch-down:1=命中可滑行(喂后续 move) */ +int meow_listview_swipe_move(meow_widget *lv, int x, int y); /* touch-move:1=消费(横向);0=让位(取消/纵向) */ +int meow_listview_swipe_end(meow_widget *lv, int x, int y); /* release:1=消费(删/回弹动画);0=未滑动(tap) */ +int meow_listview_swipe_active(meow_widget *lv); /* 1=有行处于滑动/动画中 */ +int meow_listview_swipe_offset(meow_widget *lv); /* 当前滑动行的 dx(<0=左移;自绘行画用) */ + #ifdef __cplusplus } #endif diff --git a/sdk/meow_ui_widget.c b/sdk/meow_ui_widget.c index eb727cb..5d14e92 100644 --- a/sdk/meow_ui_widget.c +++ b/sdk/meow_ui_widget.c @@ -15,7 +15,8 @@ enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3, 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_LISTVIEW = 18, /* §5.5 结构化列表(行=图标+标题+副标题+右侧操作+选中) */ + W_ICONBTN = 19 }; /* M7.0a 缺口#2:圆形/位图图标按钮(圆形命中 + 按压态) */ /* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */ #define MEOW_LAYOUT_MAX_CHILD 32 @@ -100,6 +101,8 @@ struct meow_widget { 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 模式) */ @@ -113,6 +116,21 @@ struct meow_widget { 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 时按控件内建默认)---- @@ -225,7 +243,9 @@ static int g_live_prev_hold = 0; /* 该按钮上次渲染是否为按住 static void live_btn_register(meow_widget *w) { - if (!w || w->type != W_BTN) return; + 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; @@ -246,6 +266,15 @@ static void live_btn_unregister(meow_widget *w) /* 命中:返回坐标所在的「当前 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(); @@ -253,8 +282,12 @@ static meow_widget *live_btn_hit(int x, int y) for (int i = 0; i < g_live_btn_n; i++) { meow_widget *w = g_live_btns[i]; if (w->owner != active) continue; /* 非 active activity:跳过 */ - if (x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h) + /* 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; } @@ -540,6 +573,20 @@ int meow_btn_set_variant(meow_widget *w, int variant) 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) @@ -1129,7 +1176,7 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx; - t->sel = -1; t->item_h = 56; t->scroll = 0; + t->sel = -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) { @@ -1361,6 +1408,127 @@ int meow_listview_set_empty_text(meow_widget *w, const char *text) 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; @@ -1374,9 +1542,11 @@ int meow_widget_hit(meow_widget *w, int x, int y) if (w->owner != meow_ui_activity_active()) return 0; /* §5.8.3:禁用不响应命中(也不进 pressed)。 */ if (widget_disabled(w)) return 0; - /* §5.8.1:自定义画布用 App 回调判定命中(可非矩形);其余矩形内含。 */ + /* §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) @@ -1480,6 +1650,35 @@ void meow_widget_draw(meow_widget *w) 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; @@ -1606,7 +1805,17 @@ void meow_widget_draw(meow_widget *w) 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; - w->vs_draw(i, vx0, ry, w->w, item, st, w->vs_ctx); + 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++) { @@ -1621,23 +1830,37 @@ void meow_widget_draw(meow_widget *w) 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(vx0, cl, w->w, cr - cl, th->surfaceContainerHigh); + 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(text_x, sty, it->title, text_maxw, th->font_body, th->primary); + 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(vx0, sy, w->w, 1, th->outlineVariant); + meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant); if (!it->enabled) - meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90); continue; } /* 选中/按压背景(primaryContainer / 半透明主题色按压层) */ - if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); - else if (press) meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->primary, 26); + 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; @@ -1648,38 +1871,38 @@ void meow_widget_draw(meow_widget *w) : th->onSurfaceVariant); int icy = ry + (item - isz) / 2; if (icy + isz >= vy0 && icy < vy1) - listv_draw_icon(th, vx0 + pad, icy, it->icon_kind, it->icon, iccol); + 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(text_x, ty, it->title, text_maxw, th->font_body, textcol); + 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(text_x, sy, it->subtitle, text_maxw, th->font_body, subcol); + 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(text_x, ty, it->title, text_maxw, th->font_body, textcol); + 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 = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ - if (ax >= op_start) + 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(vx0, sy, w->w, 1, th->outlineVariant); + 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(vx0, cl, w->w, cr - cl, th->onSurface, 90); + meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90); } } } else { @@ -1697,7 +1920,18 @@ void meow_widget_draw(meow_widget *w) int sel = (i == w->sel); uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; - if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); + /* 左滑删除(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 @@ -1705,38 +1939,38 @@ void meow_widget_draw(meow_widget *w) : th->onSurfaceVariant); int icy = ry + (item - isz) / 2; if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ - listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + 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(text_x, ty, it.title, text_maxw, th->font_body, textcol); + 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(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); + 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(text_x, ty, it.title, text_maxw, th->font_body, textcol); + listv_text_trunc(rtx, ty, it.title, text_maxw, th->font_body, textcol); } /* 右侧操作区 */ if (it.right != MEOW_LIST_RIGHT_NONE) { - int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ - if (ax >= op_start) + 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(vx0, sy, w->w, 1, th->outlineVariant); + 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(vx0, cl, w->w, cr - cl, th->onSurface, 90); + 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);