TopAppBar 动作项此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。改走位图: - sb_icons.h 追加 15 个 Material Symbols Rounded 动作位图(settings/refresh/search/ play_arrow/pause/skip_next/skip_previous/arrow_back/close/add/check/lyrics/ queue_music/download/heart),FILL=1 实心与状态栏一致;sb_icons[] 只追加(0-14 不变), 动作索引 15-29。 - 生成脚本 tools/gen_action_icons.py(cairosvg svg→32x32 单通道 alpha,SVG 缓存 icons_src/ 可复现、断网可跑)。 - meow_ui_gfx.c 抽取图标 blit 像素核心 icon_blit_px,新增 meow_gfx_blit_icon_ink (控件路径不标顶栏脏区);meow_gfx_blit_icon 状态栏路径行为不变。 - meow_ui_widget.c W_TOPAPPBAR 项支持位图模式:新 API meow_topappbar_set_item_icon (w, idx, icon_idx),绘制取主题 onSurface/按压向栏底色淡 12%,文本项行为不变; hit 用统一项宽。 - 公共头 widgets.h 加 MEOW_ICON_*(15-29) 枚举与 set_item_icon 声明;internal.h 声明 meow_gfx_blit_icon_ink。 - 宿主测试(test_meow_ui_widgets)加 topbar 位图图标绘制/命中/按压变淡 + 状态栏 blit 回归断言;test_umbrella 加 MEOW_ICON_* 编译期锁;MEOW_APP_SDK.md 补说明+索引表。 - 验证:宿主测试 ALL PASS(14项);交叉 zig 构建 0 warning;meow_ui.a 新 md5 1d69cdae55ab5dde3a98f5e2124f3656。
1948 lines
90 KiB
C
1948 lines
90 KiB
C
/*
|
||
* 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 <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
|
||
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 结构化列表(行=图标+标题+副标题+右侧操作+选中) */
|
||
|
||
/* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */
|
||
#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 */
|
||
|
||
/* §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.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 || w->type != W_BTN) 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 底」)。 */
|
||
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:跳过 */
|
||
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;
|
||
}
|
||
|
||
/* ---- 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->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;
|
||
}
|
||
|
||
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 回调判定命中(可非矩形);其余矩形内含。 */
|
||
if (w->type == W_CUSTOM)
|
||
return w->hit_cb ? w->hit_cb(w, x, y, w->ctx) : 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_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;
|
||
w->vs_draw(i, vx0, 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;
|
||
|
||
if (it->section) { /* 段头占位行(非粘性):surfaceContainerHigh 底 + primary 段名 */
|
||
meow_ui_fill_rect(vx0, 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);
|
||
int sy = ry + item - 1;
|
||
if (sy >= cl && sy < cr)
|
||
meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant);
|
||
if (!it->enabled)
|
||
meow_ui_fill_rect_alpha(vx0, 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);
|
||
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, vx0 + 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);
|
||
int sy = ty + thh + MEOW_LISTV_SUB_GAP;
|
||
if (sy >= vy0 && sy + thh <= vy1)
|
||
listv_text_trunc(text_x, sy, it->subtitle, text_maxw, th->font_body, subcol);
|
||
} else {
|
||
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);
|
||
}
|
||
|
||
/* 右尾件(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)
|
||
listv_draw_right(th, ax, ry, item, it->right, it->on, subcol);
|
||
}
|
||
|
||
/* 分隔线(仅落在可见带内才画) */
|
||
int sy = ry + item - 1;
|
||
if (sy >= cl && sy < cr)
|
||
meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant);
|
||
|
||
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */
|
||
if (!it->enabled)
|
||
meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90);
|
||
}
|
||
}
|
||
} 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;
|
||
if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer);
|
||
|
||
/* 图标(左) */
|
||
uint32_t iccol = sel ? th->onPrimaryContainer
|
||
: ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary
|
||
: th->onSurfaceVariant);
|
||
int icy = ry + (item - isz) / 2;
|
||
if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */
|
||
listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol);
|
||
|
||
/* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */
|
||
int ty;
|
||
if (it.subtitle && it.subtitle[0]) {
|
||
ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2;
|
||
if (it.title && ty >= vy0 && ty + thh <= vy1)
|
||
listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol);
|
||
int sy = ty + thh + MEOW_LISTV_SUB_GAP;
|
||
if (sy >= vy0 && sy + thh <= vy1)
|
||
listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol);
|
||
} else {
|
||
ty = ry + (item - thh) / 2;
|
||
if (it.title && ty >= vy0 && ty + thh <= vy1)
|
||
listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol);
|
||
}
|
||
|
||
/* 右侧操作区 */
|
||
if (it.right != MEOW_LIST_RIGHT_NONE) {
|
||
int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */
|
||
if (ax >= op_start)
|
||
listv_draw_right(th, ax, ry, item, it.right, it.on, subcol);
|
||
}
|
||
|
||
/* 分隔线(仅落在可见带内才画) */
|
||
int sy = ry + item - 1;
|
||
if (sy >= cl && sy < cr)
|
||
meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant);
|
||
|
||
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */
|
||
if (!it.enabled)
|
||
meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90);
|
||
}
|
||
}
|
||
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
|
||
break;
|
||
}
|
||
case W_CUSTOM:
|
||
/* SDK 不替自定义画布画背景/状态层;App 在 render_cb 里自绘(th 供取色),
|
||
* 可读 meow_widget_state(状态位)/ meow_widget_opts_get(通用修饰)自行叠。 */
|
||
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(嵌套:回到父控件矩形/全屏) */
|
||
}
|