diff --git a/patch_v2/hello_app/hello b/patch_v2/hello_app/hello index 78c870b..0f76929 100755 Binary files a/patch_v2/hello_app/hello and b/patch_v2/hello_app/hello differ diff --git a/patch_v2/hello_app/hello_internal.h b/patch_v2/hello_app/hello_internal.h index 7fd0b85..2c2d52f 100644 --- a/patch_v2/hello_app/hello_internal.h +++ b/patch_v2/hello_app/hello_internal.h @@ -95,11 +95,10 @@ extern rect_t r_dr1, r_dr2; /* Radio 几何 */ /* ---- 列表区 CONTAINER 子视图(ui/demo/list/list.c;批③ 视图深度化)---- * demo 页把列表逻辑区块抽成 meow_view CONTAINER 子视图挂到 DEMO ACTIVITY view children, * 承接列表交互(tap/down/move/swipe)、列表视图态与列表区块标题 on_draw 直绘。 - * 嵌套生命周期(list_view_enter/leave)由 demo 页 on_enter/on_leave 驱动(SDK 骨架不递归)。 */ + * 嵌套生命周期由 SDK view_enter/view_leave 递归驱动(随 demo 页进出;list_view_enter/leave + * 已退役,无需 App 层显式接管)。 */ meow_view *list_view_create(void); /* 建 CONTAINER 子视图(须即 meow_view_add_child) */ meow_view *list_view_get(void); /* 返回子视图(宿主测嵌套 on_enter/on_leave 断言) */ -void list_view_enter(void); /* 进入子视图(置 ENTERED + on_enter,幂等) */ -void list_view_leave(void); /* 离开子视图(清 ENTERED + on_leave,幂等) */ void list_view_bind(meow_widget *w, rect_t list, rect_t title); /* 内容重建后绑控件+几何+恢复态 */ void list_view_release(void); /* 内容释放后摘引用/几何 */ int list_view_hit(int x, int y); /* tap 分发给列表区,命中即消费(含选中) */ diff --git a/patch_v2/hello_app/ui/demo/demo.c b/patch_v2/hello_app/ui/demo/demo.c index ac6e6e7..ae5175a 100644 --- a/patch_v2/hello_app/ui/demo/demo.c +++ b/patch_v2/hello_app/ui/demo/demo.c @@ -408,7 +408,8 @@ static void demo_page_enter(meow_view *v, void *ctx) ((demo_ctrl *)ctx)->active = 1; g_demo = 1; g_demo_focus = 0; - list_view_enter(); /* 先进入列表区子视图(其 on_draw 在下面 rebuild→paint 中经注册表触发) */ + /* 列表区 CONTAINER 子视图随本页进入由 SDK view_enter 递归驱动(其 on_draw 在 demo_rebuild_to + * →demo_paint 中经注册表绘制;meow_draw_view_entered 含祖先已进入判定,首批可见)。 */ demo_rebuild_to(0); meow_log(MEOW_LOG_INFO, "demo enter scroll=0"); } @@ -418,7 +419,7 @@ static void demo_page_leave(meow_view *v, void *ctx) (void)v; ((demo_ctrl *)ctx)->active = 0; g_demo = 0; - list_view_leave(); /* 子离开先于父离开(嵌套顺序) */ + /* 列表区子视图由 SDK view_leave 在父之前递归离开(子<先>父,§A.4/D.1)。 */ } static void demo_page_destroy(meow_view *v, void *ctx) diff --git a/patch_v2/hello_app/ui/demo/list/list.c b/patch_v2/hello_app/ui/demo/list/list.c index e102546..220e28e 100644 --- a/patch_v2/hello_app/ui/demo/list/list.c +++ b/patch_v2/hello_app/ui/demo/list/list.c @@ -13,8 +13,9 @@ * 实现「子>父」命中。list **控件创建/释放**仍留在 demo 页内容重建流(demo_build_at/ * demo_free_content),因为 widget 由 z 序注册表 auto 注册/注销、并随 scroll 重建——完整 * 的「控件归容器拥有」需 SDK 侧支持(本批冻结 patch_v2/sdk/),故以绑定引用的方式折中。 - * - SDK 骨架(meow_ui_view.c)的 view_enter/view_leave 不递归 children(批次① 仅顶层页); - * 本子视图的 on_enter/on_leave 由 demo 页 on_enter/on_leave 显式驱动(list_view_enter/leave)。 + * - SDK(meow_ui_view.c)的 view_enter/view_leave **递归 children**(批次①→③ 后接线): + * 本子视图随 demo 页 on_enter/on_leave 自动进入/离开,无需 App 层显式驱动(list_view_enter/leave + * 已退役);父 on_enter 重绘时子视图经 meow_draw_view_entered(含祖先已进入)可见。 * - on_draw 是回调、view 不缓存像素(非保留,§5.8.7);绘制由 meow_ui_paint_full/invalidate * 触发,仅当本子视图 state 含 MEOW_VIEW_STATE_ENTERED 时注册表才绘制(meow_draw_view_entered)。 */ @@ -66,24 +67,8 @@ static void list_page_destroy(meow_view *v, void *ctx) g_list_view = NULL; /* meow_view_free 递归释放前清引用,防悬垂 */ } -/* 进入子视图:置 ENTERED + on_enter(幂等)。SDK view_enter 不递归,故由父页显式驱动。 */ -void list_view_enter(void) -{ - if (!g_list_view) return; - if (g_list_view->state & MEOW_VIEW_STATE_ENTERED) return; - g_list_view->state |= MEOW_VIEW_STATE_ENTERED; - if (g_list_view->hooks.on_enter) g_list_view->hooks.on_enter(g_list_view, g_list_view->ctx); -} -/* 离开子视图:清 ENTERED + on_leave(幂等)。 */ -void list_view_leave(void) -{ - if (!g_list_view) return; - if (!(g_list_view->state & MEOW_VIEW_STATE_ENTERED)) return; - g_list_view->state &= ~MEOW_VIEW_STATE_ENTERED; - if (g_list_view->hooks.on_leave) g_list_view->hooks.on_leave(g_list_view, g_list_view->ctx); -} - -/* 建 CONTAINER 子视图(demo_page_create 调,随即 meow_view_add_child 到 demo ACTIVITY view)。 */ +/* 建 CONTAINER 子视图(demo_page_create 调,随即 meow_view_add_child 到 demo ACTIVITY view)。 + * 进入/离开经 SDK view_enter/view_leave 递归驱动(随 demo 页),本文件不再显式接管。 */ meow_view *list_view_create(void) { static const meow_view_hooks h = { diff --git a/patch_v2/sdk/meow_ui_draw.c b/patch_v2/sdk/meow_ui_draw.c index fc25145..7ffcd4e 100644 --- a/patch_v2/sdk/meow_ui_draw.c +++ b/patch_v2/sdk/meow_ui_draw.c @@ -243,7 +243,15 @@ void meow_draw_view_unregister(struct meow_view *v) } int meow_draw_view_entered(const struct meow_view *v) { - return v ? meow_view_get_state(v) & MEOW_VIEW_STATE_ENTERED : 0; + /* 子>父:view 或其任意祖先已进入即视为「可绘制」——配合 view_enter/leave 递归 + * (meow_ui_view.c §A.4),父 on_enter 重绘时子视图(尚未单独进入)也要经 on_draw 画出; + * 且子随父进出("子视图随父进出"),祖先已进入 ⇒ 子树已进入,可绘制。 */ + const struct meow_view *cur = v; + while (cur) { + if (meow_view_get_state(cur) & MEOW_VIEW_STATE_ENTERED) return 1; + cur = meow_view_parent(cur); + } + return 0; } void meow_draw_view_set_z(struct meow_view *v, int z) { diff --git a/patch_v2/sdk/meow_ui_gfx.c b/patch_v2/sdk/meow_ui_gfx.c index e2dd313..bc4c8ca 100644 --- a/patch_v2/sdk/meow_ui_gfx.c +++ b/patch_v2/sdk/meow_ui_gfx.c @@ -62,6 +62,48 @@ static int g_dirty[/*MUI_DIRTY_MAX*/12][4]; /* [x,y,w,h] */ static int g_ndirty = 0; static int g_dirty_full = 1; +/* ---- 绘制期裁剪(clip rect,§3;默认全屏)---- + * 栈式嵌套:push 与当前 clip 求交后压栈,pop 恢复。写像素原语(put_px / put_px_blend / + * fill_rect / fill_rect_alpha / round / circle / ring / blit_icon / blit_rgba / text) + * 统一过 g_clip;顶层(g_clip_n==0)= 全屏(g_xres×g_yres),故 IME/模态的全屏 scrim + * 直绘不受裁剪。 */ +#define MUI_CLIP_MAX 16 +static int g_clip_stack[MUI_CLIP_MAX][4]; /* [x0,y0,x1,y1](闭开区间) */ +static int g_clip_n = 0; + +static void clip_cur(int out[4]) { /* 当前 clip 边界(无 clip = 全屏) */ + if (g_clip_n > 0) { + out[0] = g_clip_stack[g_clip_n - 1][0]; out[1] = g_clip_stack[g_clip_n - 1][1]; + out[2] = g_clip_stack[g_clip_n - 1][2]; out[3] = g_clip_stack[g_clip_n - 1][3]; + } else { out[0] = 0; out[1] = 0; out[2] = g_xres; out[3] = g_yres; } +} +static int clip_contains(int x, int y) +{ + if (g_clip_n == 0) return x >= 0 && x < g_xres && y >= 0 && y < g_yres; + return x >= g_clip_stack[g_clip_n - 1][0] && x < g_clip_stack[g_clip_n - 1][2] && + y >= g_clip_stack[g_clip_n - 1][1] && y < g_clip_stack[g_clip_n - 1][3]; +} + +void meow_gfx_clip_push(int x, int y, int w, int h) +{ + if (!g_init_ok || w <= 0 || h <= 0) return; + int c[4]; clip_cur(c); + int ix0 = x > c[0] ? x : c[0]; + int iy0 = y > c[1] ? y : c[1]; + int ix1 = (x + w) < c[2] ? (x + w) : c[2]; + int iy1 = (y + h) < c[3] ? (y + h) : c[3]; + if (ix1 < ix0) ix1 = ix0; + if (iy1 < iy0) iy1 = iy0; + if (g_clip_n >= MUI_CLIP_MAX) return; /* 栈满:忽略本次 push(保持当前 clip) */ + g_clip_stack[g_clip_n][0] = ix0; g_clip_stack[g_clip_n][1] = iy0; + g_clip_stack[g_clip_n][2] = ix1; g_clip_stack[g_clip_n][3] = iy1; + g_clip_n++; +} +void meow_gfx_clip_pop(void) +{ + if (g_clip_n > 0) g_clip_n--; +} + static char *g_dump_path = NULL; /* ---- 状态栏「上收出屏只留提示线」(collapsed,基座)---- @@ -158,6 +200,17 @@ static void dirty_add(int x, int y, int w, int h) if (x + w > g_xres) w = g_xres - x; if (y + h > g_yres) h = g_yres - y; if (w <= 0 || h <= 0) return; + /* 绘制期裁剪:脏区也不越 clip(顶层 clip=全屏 → 无操作)。 */ + if (g_clip_n > 0) { + int *c = g_clip_stack[g_clip_n - 1]; + int x1 = x + w, y1 = y + h; + if (x < c[0]) x = c[0]; + if (y < c[1]) y = c[1]; + if (x1 > c[2]) x1 = c[2]; + if (y1 > c[3]) y1 = c[3]; + if (x1 <= x || y1 <= y) return; + w = x1 - x; h = y1 - y; + } for (int i = 0; i < g_ndirty; i++) { if (rect_hit(g_dirty[i][0], g_dirty[i][1], g_dirty[i][2], g_dirty[i][3], x, y, w, h)) { int nx1 = g_dirty[i][0] < x ? g_dirty[i][0] : x; @@ -237,6 +290,7 @@ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) for (int dx = 0; dx < t_w; dx++) { int tx = x0 + dx; if (tx < 0 || tx >= g_xres) continue; + if (!clip_contains(tx, ty)) continue; int sx = minx + (dx * ink_w) / t_w; int a = src[sy * SB_ICON_SIZE + sx]; if (a < 8) continue; /* 透明像素跳过 */ @@ -265,7 +319,7 @@ static uint16_t px_read_rgb565(int x, int y) static void put_px(int x, int y, uint32_t color) { - if (x < 0 || x >= g_xres || y < 0 || y >= g_yres) return; + if (!clip_contains(x, y)) return; if (g_bpp == 16) { px_write_rgb565(x, y, rgb565(color)); } else { @@ -276,7 +330,7 @@ static void put_px(int x, int y, uint32_t color) static void put_px_blend(int x, int y, uint32_t color, int alpha) { - if (x < 0 || x >= g_xres || y < 0 || y >= g_yres) return; + if (!clip_contains(x, y)) return; uint16_t fg = rgb565(color); if (g_bpp == 16) { uint16_t bg = px_read_rgb565(x, y); @@ -300,6 +354,10 @@ static void put_px_blend(int x, int y, uint32_t color, int alpha) void meow_ui_clear(uint32_t color) { if (!g_init_ok) return; + if (g_clip_n > 0) { /* 绘制期裁剪:清当前 clip 区(顶层 clip=全屏 → 走快速整刷) */ + meow_ui_fill_rect(0, 0, g_xres, g_yres, color); + return; + } /* 快速整刷(16bit 用半字填充,可 memset 当色值由 32bit 拼接掩码) */ if (g_bpp == 16) { uint16_t c16 = rgb565(color); @@ -327,6 +385,16 @@ void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color) if (x + w > g_xres) w = g_xres - x; if (y + h > g_yres) h = g_yres - y; if (w <= 0 || h <= 0) return; + if (g_clip_n > 0) { /* 绘制期裁剪:与当前 clip 求交(顶层 clip=全屏 → 无操作) */ + int *c = g_clip_stack[g_clip_n - 1]; + int x1 = x + w, y1 = y + h; + if (x < c[0]) x = c[0]; + if (y < c[1]) y = c[1]; + if (x1 > c[2]) x1 = c[2]; + if (y1 > c[3]) y1 = c[3]; + if (x1 <= x || y1 <= y) return; + w = x1 - x; h = y1 - y; + } if (g_bpp == 16) { uint16_t c16 = rgb565(color); for (int ry = y; ry < y + h; ry++) { @@ -534,6 +602,16 @@ void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alp if (x + w > g_xres) w = g_xres - x; if (y + h > g_yres) h = g_yres - y; if (w <= 0 || h <= 0) return; + if (g_clip_n > 0) { /* 绘制期裁剪:与当前 clip 求交(顶层 clip=全屏 → 无操作) */ + int *c = g_clip_stack[g_clip_n - 1]; + int x1 = x + w, y1 = y + h; + if (x < c[0]) x = c[0]; + if (y < c[1]) y = c[1]; + if (x1 > c[2]) x1 = c[2]; + if (y1 > c[3]) y1 = c[3]; + if (x1 <= x || y1 <= y) return; + w = x1 - x; h = y1 - y; + } for (int ry = y; ry < y + h; ry++) for (int rx = x; rx < x + w; rx++) put_px_blend(rx, ry, color, alpha); dirty_add(x, y, w, h); @@ -677,13 +755,28 @@ static void text_draw(int x, int y, const char *s, int size, uint32_t color) { if (!g_init_ok || !s) return; if (g_mfont_ready && g_bpp == 16) { - meow_font_drawtext((uint16_t *)g_fb, x, y, s, rgb565(color), size); int w = meow_font_width_text(s, size); int hh = meow_font_line_height(size); + if (g_clip_n > 0) { + int *c = g_clip_stack[g_clip_n - 1]; + if (x + w <= c[0] || x >= c[2] || y + hh <= c[1] || y >= c[3]) return; /* 与 clip 无交 */ + if (!(x >= c[0] && x + w <= c[2] && y >= c[1] && y + hh <= c[3])) { + /* 部分相交:用「偏移 fb 子矩形 + 临时 fb 几何」做裁剪(meow_font 只按全屏剪)。 + * 把 fb 基址指向 clip 左上角、fb_w/h 设为 clip 尺寸、原点平移到 clip 坐标, + * 这样 meow_font 的逐字形裁剪天然落在 clip 内;画完恢复全屏几何。 */ + uint16_t *fb = (uint16_t *)(g_fb + (size_t)c[1] * g_line + (size_t)c[0] * 2); + meow_font_set_fb((unsigned)(c[2] - c[0]), (unsigned)(c[3] - c[1]), (unsigned)g_line); + meow_font_drawtext(fb, x - c[0], y - c[1], s, rgb565(color), size); + meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); + dirty_add(x, y, w, hh); + return; + } + } + meow_font_drawtext((uint16_t *)g_fb, x, y, s, rgb565(color), size); dirty_add(x, y, w, hh); } else { int sc = fallback_scale(size); - draw_text5x7(x, y, s, sc, color); + draw_text5x7(x, y, s, sc, color); /* 走 put_px,自动过 clip */ dirty_add(x, y, (int)strlen(s) * 6 * sc, 7 * sc); } } diff --git a/patch_v2/sdk/meow_ui_internal.h b/patch_v2/sdk/meow_ui_internal.h index 2dac50f..2d22645 100644 --- a/patch_v2/sdk/meow_ui_internal.h +++ b/patch_v2/sdk/meow_ui_internal.h @@ -102,6 +102,14 @@ void meow_gfx_mark_dirty(int x, int y, int w, int h); * 解除后由调用方一次 flush 原子上屏,避免覆盖层与 App on_tick 重绘同帧两刷频闪。 */ void meow_gfx_flush_hold(int on); +/* 绘制期裁剪(MEOW_APP_SDK.md §3,2026-08-31):当前 clip 矩形内才写像素,默认全屏。 + * meow_widget_draw(w) 画任何控件前 push 该控件矩形、画完 pop——任何控件(含 W_CUSTOM) + * 画不出自身边界,根治自定义画布越界脏区。可嵌套(入栈/出栈,push 与当前 clip 求交)。 + * 顶层 clip=全屏,故 IME/模态(snapshot+整帧路径)经 gfx 原语直绘全屏 scrim 时不受裁剪; + * meow_gfx_flush_hold 语义不依赖 clip(flush 只在解除后原子上屏)。 */ +void meow_gfx_clip_push(int x, int y, int w, int h); +void meow_gfx_clip_pop(void); + /* 标记整帧脏(launcher 手动脏模型桥接:自绘 raw 像素后标全屏,present 走 SDK)。 */ void meow_gfx_mark_full(void); diff --git a/patch_v2/sdk/meow_ui_log.c b/patch_v2/sdk/meow_ui_log.c index 39342c5..d0981c4 100644 --- a/patch_v2/sdk/meow_ui_log.c +++ b/patch_v2/sdk/meow_ui_log.c @@ -5,9 +5,9 @@ * fopen/fprintf),重复、易错、无分离/级别。本文件实现 SDK 统一 meow_log(): * * - app 一行调用 meow_log(MEOW_LOG_INFO, "start")(见 meow_ui.h); - * - SDK 启动时读 env:MEOW_LOG_DIR(日志目录)、MEOW_LOG_APP(app_id,无则 "app")、 - * MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写 - * `/apps//logs/<启动时间>_[._].log`(目录自动 mkdir -p), + * - SDK 启动时读 env:MEOW_LOG_DIR(日志目录,launcher 已注入到 logs/,含 app 分层)、 + * MEOW_LOG_APP(app_id,无则 "app")、MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写 + * `/<启动时间>_[._].log`(目录自动 mkdir -p), * 按 app 分目录 + 每次启动新文件(不同启动=不同文件,同一次启动内 O_APPEND 追加), * 级别过滤(lvl < level 丢弃); * - 格式:` [] message\n`(meow_ui_time_ms() 取毫秒); @@ -57,8 +57,8 @@ static char g_app_id[64] = "app"; /* env MEOW_LOG_APP,无则 "app" */ static char g_log_dir[256] = ""; /* env MEOW_LOG_DIR,无/空则降级 stdio */ static meow_log_level g_level = MEOW_LOG_INFO; /* 默认 INFO */ -/* M2.52:日志文件为 `/apps//logs/_[._].log`。 - * g_log_base 存不含序号后缀的路径前缀 `/apps//logs/_`; +/* M2.52:日志文件为 `/_[._].log`;M2.x 简化后 MEOW_LOG_DIR 已含 app 分层 + * (launcher 注入 logs/)。g_log_base 存不含序号后缀的路径前缀 `/_`; * g_log_seq 为轮转序号(0=首个文件无后缀;>0 追加 `_`)。两者均在 log_init() * (pthread_once 那次)里生成一次。 */ static char g_log_base[384] = ""; @@ -93,7 +93,7 @@ static void log_init(void) s = getenv("MEOW_LOG_DIR"); if (s && s[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", s); - /* M2.52:构建完整前缀 `/apps//logs/_`。 + /* M2.52:构建完整前缀 `/_`(M2.x 简化:MEOW_LOG_DIR 已含 app 分层)。 * ts 用 localtime 的 %Y%m%d_%H%M%S(启动时刻);尾拼 _ 防同秒重复启动撞名, * 从而每次启动=新文件,同一次启动内 O_APPEND 追加。若 log_dir 为空(无卡) * 则跳过(log_open 降级 stderr)。 */ @@ -103,8 +103,8 @@ static void log_init(void) struct tm tmnow; localtime_r(&now, &tmnow); strftime(ts, sizeof ts, "%Y%m%d_%H%M%S", &tmnow); - snprintf(g_log_base, sizeof g_log_base, "%s/apps/%s/logs/%s_%d", - g_log_dir, g_app_id, ts, (int)getpid()); + snprintf(g_log_base, sizeof g_log_base, "%s/%s_%d", + g_log_dir, ts, (int)getpid()); log_rebuild_file(); } @@ -138,7 +138,7 @@ static int log_mkdir_p(const char *path) return 0; } -/* M2.52:对 `/apps//logs` 增量 mkdir -p,并打开 g_log_file +/* M2.52:对 ``(M2.x 简化后已含 app 分层)增量 mkdir -p,并打开 g_log_file * (O_APPEND|O_CREAT;同一次启动内追加,不同启动=不同文件)。失败置 g_log_fallback。 * 首次由 meow_log 经 g_log_open_tried 守卫调用;轮转后由 meow_log 再次调用(目录已存在, * mkdir_p 幂等)。仅在持有 g_log_mu 时调用(g_log_base 已构建)。 */ @@ -146,7 +146,7 @@ static void log_open(void) { char logs[512]; if (!g_log_dir[0] || !g_log_file[0]) { g_log_fallback = 1; return; } - if (snprintf(logs, sizeof logs, "%s/apps/%s/logs", g_log_dir, g_app_id) >= (int)sizeof logs) { + if (snprintf(logs, sizeof logs, "%s", g_log_dir) >= (int)sizeof logs) { g_log_fallback = 1; return; } if (log_mkdir_p(logs) != 0) { g_log_fallback = 1; return; } @@ -165,7 +165,7 @@ static int log_spill_cmp(const void *a, const void *b) return 0; } -/* 每 App 总量清理:扫描 `/apps//logs/` 下所有 .log(跳过当前正写的 +/* 每 App 总量清理:扫描 `/`(M2.x 简化后已含 app 分层)下所有 .log(跳过当前正写的 * g_log_file),累计大小,超过 MEOW_LOG_TOTAL_MAX 则按 mtime 从最旧删到 ≤ 阈值。 * 只规整本 App 目录(进程隔离),节流调用(见 meow_log)。仅在持有 g_log_mu 时调用。 */ static void log_cleanup_old(void) @@ -178,7 +178,7 @@ static void log_cleanup_old(void) long total = 0; if (!g_log_dir[0] || !g_log_file[0]) return; - if (snprintf(logs, sizeof logs, "%s/apps/%s/logs", g_log_dir, g_app_id) + if (snprintf(logs, sizeof logs, "%s", g_log_dir) >= (int)sizeof logs) return; d = opendir(logs); if (!d) return; diff --git a/patch_v2/sdk/meow_ui_view.c b/patch_v2/sdk/meow_ui_view.c index 9ab349d..e1dc8e1 100644 --- a/patch_v2/sdk/meow_ui_view.c +++ b/patch_v2/sdk/meow_ui_view.c @@ -40,17 +40,24 @@ static meow_view *view_map_get(int id) return (id >= 0 && id < MEOW_VIEW_BY_ID_MAX) ? g_view_by_id[id] : NULL; } -/* 进入一个 view(置 ENTERED + on_enter)。 */ +/* 进入一个 view(置 ENTERED + on_enter),**递归 children**(子视图随父进出,§A.4)。 + * 进入顺序:父 on_enter 先(记录顺序 home.enter→demo.enter),随后子视图依递归逐个进入; + * 子视图的 on_draw 经 meow_draw_view_entered(含祖先已进入判定)保证父 on_enter 重绘时可见。 */ static void view_enter(meow_view *v) { if (!v || (v->state & MEOW_VIEW_STATE_ENTERED)) return; v->state |= MEOW_VIEW_STATE_ENTERED; run_fn(v->hooks.on_enter, v); + for (int i = 0; i < v->nchildren; i++) + view_enter(v->children[i]); } -/* 离开一个 view(清 ENTERED + on_leave;默认不销毁子 view/widget——弹回零重建)。 */ +/* 离开一个 view(清 ENTERED + on_leave;默认不销毁子 view/widget——弹回零重建), + * **递归 children**:先子后父(demo.leave→home.leave)。 */ static void view_leave(meow_view *v) { if (!v || !(v->state & MEOW_VIEW_STATE_ENTERED)) return; + for (int i = 0; i < v->nchildren; i++) + view_leave(v->children[i]); v->state &= ~MEOW_VIEW_STATE_ENTERED; run_fn(v->hooks.on_leave, v); } @@ -175,6 +182,36 @@ meow_view *meow_view_sub_pop(meow_view *v) return NULL; /* 预留 */ } +/* ============================================================ */ +/* 事件路由(§A.7:每个 view 的 on_event 先把事件发给后代(按 z 降序),命中即消费, + * 再落本 view 的控件)。SDK 提供递归树走;叶子 on_event 负责本 view 的 widget 命中。 */ +/* ============================================================ */ +static int view_dispatch_event(meow_view *v, int ev, int a, int b) +{ + if (!v || !(v->state & MEOW_VIEW_STATE_ENTERED)) return 0; /* 仅已进入的视图参与 */ + if (v->nchildren > 0) { + meow_view *kids[MEOW_VIEW_MAX_CHILD]; + int nn = v->nchildren; + for (int i = 0; i < nn; i++) kids[i] = v->children[i]; + /* 按 z 降序(同 z 保持挂载序稳定);z 同值时树序,靠后挂的在先。 */ + for (int i = 1; i < nn; i++) { + meow_view *key = kids[i]; + int j = i - 1; + while (j >= 0 && meow_view_z(kids[j]) < meow_view_z(key)) { kids[j + 1] = kids[j]; j--; } + kids[j + 1] = key; + } + for (int i = 0; i < nn; i++) + if (view_dispatch_event(kids[i], ev, a, b)) return 1; /* 子命中即消费 */ + } + if (v->hooks.on_event && v->hooks.on_event(v, ev, a, b, v->ctx)) return 1; /* 落到本 view 事件 */ + return 0; +} + +int meow_view_dispatch_event(int ev, int a, int b) +{ + return view_dispatch_event(g_top_view, ev, a, b); +} + /* ============================================================ */ /* 坐标 / 查询 */ /* ============================================================ */ diff --git a/patch_v2/sdk/meow_ui_view.h b/patch_v2/sdk/meow_ui_view.h index 0b221b9..729d703 100644 --- a/patch_v2/sdk/meow_ui_view.h +++ b/patch_v2/sdk/meow_ui_view.h @@ -80,6 +80,11 @@ void meow_view_page_push(int id); /* activity_push + 旧top on_leav void meow_view_page_pop(void); /* 旧top on_leave + activity_pop + 新top on_enter */ void meow_view_page_replace(int id); /* 旧top on_leave + 新页 on_enter(目标页须已 create) */ +/* 入口:把语义事件注入当前活动页 view 子树路由——先发子视图(按 z 降序),命中即消费, + * 再落本 view 的 on_event(其内做该 view 的 widget 命中)。返回 1=已消费(§A.7)。 + * 未接线于 meow_ui_run(App 可在 on_tap/on_swipe 等回调里改用它实现 SDK 原生路由)。 */ +int meow_view_dispatch_event(int ev, int a, int b); + /* ---- 页内子导航(预留扩展位 a;本批为空实现/桩,仅在 ACTIVITY view 内维护子栈)---- */ int meow_view_sub_push(meow_view *v, meow_view *sub); /* 预留;返回 0(未实现于本批) */ meow_view *meow_view_sub_pop(meow_view *v); /* 预留;返回 NULL(未实现于本批) */ diff --git a/patch_v2/sdk/meow_ui_widget.c b/patch_v2/sdk/meow_ui_widget.c index e3fc97b..4f8ef37 100644 --- a/patch_v2/sdk/meow_ui_widget.c +++ b/patch_v2/sdk/meow_ui_widget.c @@ -1204,6 +1204,10 @@ int meow_switch_get(meow_widget *w) { return w && w->type == W_SWITCH ? w->on : 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); @@ -1616,4 +1620,5 @@ void meow_widget_draw(meow_widget *w) default: break; } + meow_gfx_clip_pop(); /* 恢复进入前的 clip(嵌套:回到父控件矩形/全屏) */ } diff --git a/patch_v2/sdk/test_ime_fix b/patch_v2/sdk/test_ime_fix index 87d348a..14f793e 100755 Binary files a/patch_v2/sdk/test_ime_fix and b/patch_v2/sdk/test_ime_fix differ diff --git a/patch_v2/sdk/test_meow_ui_widgets b/patch_v2/sdk/test_meow_ui_widgets index 6b0b860..2e3cf4f 100755 Binary files a/patch_v2/sdk/test_meow_ui_widgets and b/patch_v2/sdk/test_meow_ui_widgets differ diff --git a/patch_v2/sdk/test_meow_ui_widgets.c b/patch_v2/sdk/test_meow_ui_widgets.c index 266edb8..6dae3d2 100644 --- a/patch_v2/sdk/test_meow_ui_widgets.c +++ b/patch_v2/sdk/test_meow_ui_widgets.c @@ -14,6 +14,7 @@ */ #include "meow_ui.h" #include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ +#include "meow_ui_view.h" /* 任务② view 递归 + on_event 路由断言 */ #include #include #include @@ -95,6 +96,31 @@ static void free_all(void) /* §6.3 通用长按测试回调(开闸长按判定用;事件本身经 meow_input_pop 断言)。 */ static void lp_recv(int x, int y) { (void)x; (void)y; } +/* ---- 任务① 绘制期 clip:W_CUSTOM 越界绘制被裁掉(render 回调经 flag 切换越界)---- + * 控件几何硬编码为 (40,40,80,40)(与下方创建一致):meow_widget 是对外不透明类型, + * 测试经 meow_widget_custom_create(40,40,80,40,...) 建在同址,回调按常量绘制。 */ +static int g_custom_oob = 1; /* 1=画越界实心块+越界文本;0=只画控件内 */ +static void clip_oob_render(meow_widget *w, const meow_theme *th, void *ctx) +{ + (void)w; (void)th; (void)ctx; + meow_ui_fill_rect(45, 45, 20, 20, 0xFF0000); /* 控件 (40,40,80,40) 内:必画红块 */ + if (g_custom_oob) { + /* 越界(右/下,出控件矩形 [40..120)×[40..80)):应被 widget clip 裁掉 */ + meow_ui_fill_rect(140, 40, 60, 40, 0xFF0000); + meow_ui_text(160, 100, "OOB", 20, 0xFF0000); + } +} + +/* ---- 任务② view 递归 enter/leave + on_event 子>父:trace 顺序 + 命中计数 ---- */ +static int g_vtrace[16]; static int g_vtrace_n = 0; +static void vtrace_enter(meow_view *v, void *ctx) { (void)ctx; g_vtrace[g_vtrace_n++] = v->id; } +static void vtrace_leave(meow_view *v, void *ctx) { (void)ctx; g_vtrace[g_vtrace_n++] = -v->id; } +static int g_ev_child_hits = 0, g_ev_parent_hits = 0; +static int vev_child(meow_view *v, int ev, int a, int b, void *ctx) +{ (void)v; (void)ctx; if (ev == 7 && a == 10 && b == 10) { g_ev_child_hits++; return 1; } return 0; } +static int vev_parent(meow_view *v, int ev, int a, int b, void *ctx) +{ (void)v; (void)ctx; if (ev == 7) { g_ev_parent_hits++; return 1; } return 0; } + int main(void) { setenv("MEOW_UI_DUMP", "/tmp/b2.ppm", 1); @@ -361,6 +387,52 @@ int main(void) printf("PASS long-press: trigger/suppress-tap/short-tap/cancel-move/configurable/opt-in\n"); } + /* ---------- 任务① 绘制期 clip:W_CUSTOM 越界绘制被裁掉 ---------- */ + { + const meow_theme *thc = meow_theme_get(); + int bg_r, bg_g, bg_b; UNPK(thc->bg, bg_r, bg_g, bg_b); + meow_widget *cw = W(meow_widget_custom_create(40, 40, 80, 40, clip_oob_render, NULL, NULL)); + /* a) clip 生效:经 meow_widget_draw(push 控件矩形)→ 越界块/文本被裁掉 */ + g_custom_oob = 1; + meow_ui_clear(thc->bg); + meow_widget_draw(cw); + assert(region_has(45, 45, 20, 20, 0xFF, 0x00, 0x00) > 30); /* 控件内红块画出 */ + assert(px_near(40 + 80 + 25, 40 + 10, bg_r, bg_g, bg_b)); /* 越界实心块被裁 → bg */ + assert(px_near(40 + 80 + 45, 40 + 65, bg_r, bg_g, bg_b)); /* 越界文本被裁 → bg */ + /* b) 对照:无 clip 直绘同尺寸块会画出(证明是 clip 在裁) */ + meow_ui_clear(thc->bg); + meow_ui_fill_rect(40 + 80 + 20, 40, 60, 40, 0x00FF00); + assert(region_has(40 + 80 + 20, 40, 60, 40, 0x00, 0xFF, 0x00) > 100); + printf("PASS clip: W_CUSTOM out-of-bounds fill+text clipped by widget clip\n"); + } + + /* ---------- 任务② view 递归 enter/leave + on_event 子>父命中 ---------- */ + { + static const meow_view_hooks ph = { .on_enter = vtrace_enter, .on_leave = vtrace_leave, + .on_event = vev_parent }; + static const meow_view_hooks ch = { .on_enter = vtrace_enter, .on_leave = vtrace_leave, + .on_event = vev_child }; + /* 页 id 须 < MEOW_VIEW_BY_ID_MAX(32) 才能进 g_view_by_id(page_push 查找) */ + meow_view *pv = meow_view_create(3, MEOW_VIEW_ACTIVITY, &ph, NULL); + meow_view *cv = meow_view_create(4, MEOW_VIEW_CONTAINER, &ch, NULL); + assert(meow_view_add_child(pv, cv) == 0); + g_vtrace_n = 0; g_ev_child_hits = 0; g_ev_parent_hits = 0; + meow_view_page_push(3); /* 进入 pv → 递归进 cv:pv.on_enter→cv.on_enter */ + assert(g_vtrace_n == 2 && g_vtrace[0] == 3 && g_vtrace[1] == 4); + /* 命中 (10,10):先发子(按 z 降序),子命中即消费,父不再收 */ + assert(meow_view_dispatch_event(7, 10, 10) == 1); + assert(g_ev_child_hits == 1 && g_ev_parent_hits == 0); + /* 命中 (20,20):子不命中 → 落父 on_event */ + assert(meow_view_dispatch_event(7, 20, 20) == 1); + assert(g_ev_child_hits == 1 && g_ev_parent_hits == 1); + /* 离开 pv → 递归离开:cv.on_leave(-4) → pv.on_leave(-3) */ + g_vtrace_n = 0; + meow_view_page_pop(); + assert(g_vtrace_n == 2 && g_vtrace[0] == -4 && g_vtrace[1] == -3); + meow_view_free(pv); /* 递归释放 cv */ + printf("PASS view: recursive enter/leave order + on_event child>parent hit\n"); + } + /* ---------- 绘制所有控件到 back_buf(5x7 兜底),flush 出屏幕 ---------- */ const meow_theme *th = meow_theme_get(); meow_ui_clear(th->bg);