/* ui/viewer.c — 文本预览页(ACT_VIEWER,子页)view + 控制器。 * * 只读、可滚动、≤256KB 截断(APP_READ_MAX,超长只读前 256KB)。 * 行模型:把读入内容按 '\n' 切成逻辑行(<= VIEWER_MAX_LINES,超长截断并附加提示); * 每逻辑行按可视列宽软换行成「屏幕行」;滚动按屏幕行像素仿真化(只画可见屏幕行)。 * 打开时 meow_view_page_push(ACT_VIEWER)(on_enter 置标题/清滚动/整帧重画);返回=左缘左滑/返回钮。 */ #include "files_internal.h" #include #include #include #include #include typedef struct { meow_view *view; int active; } viewer_ctrl; static viewer_ctrl g_vc; #define VIEWER_MAX_LINES 8192 /* 逻辑行上限(超长折行会很多,但按行数界内存) */ static char *g_buf; /* malloc'd 读入内容(前 256KB,NUL 结尾) */ static size_t g_buf_len; static char g_fname[FSX_MAX_NAME]; static int *g_line; /* g_nline 个:每逻辑行起始偏移 */ static int g_nline; static int *g_rown; /* g_nline 个:每逻辑行对应的首「屏幕行」下标 */ static int g_nrows; /* 总屏幕行数(= 末行 rown + 其折行数) */ static int g_vscroll; /* 滚动偏移(px) */ static int g_vdrag, g_vdrag_last_y; static meow_widget *g_tb; static meow_widget *g_vcw; /* 内容区 custom widget(画文本行) */ static app_rect_t r_content; /* 可视列宽(像素)。 */ static int viewer_colw(void) { return r_content.w - 2 * APP_PAD; } /* 文本行区视口高(底部留页脚)。 */ static int viewer_vp_h(void) { return r_content.h - APP_FOOTER_H; } /* 屏幕行高(px)。 */ static int viewer_rowh(const meow_theme *th) { return meow_ui_text_height(th->font_body) + 6; } /* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。 */ static int viewer_wrap_line(int li, int intra, char *out, int outsz) { if (!g_buf || li < 0 || li >= g_nline) return 0; int start = g_line[li]; int end = (li + 1 < g_nline) ? g_line[li + 1] : (int)g_buf_len; int len = end - start; while (len > 0 && (g_buf[start + len - 1] == '\n' || g_buf[start + len - 1] == '\r')) len--; int colw = viewer_colw(); int col = 0, row = 0, outlen = 0; char ch[2]; ch[1] = '\0'; for (int i = 0; i < len; i++) { ch[0] = g_buf[start + i]; if (ch[0] == '\r') continue; int w = meow_ui_text_width(ch, 20); if (col + w > colw && col > 0) { row++; col = 0; } if (row == intra && out && outlen < outsz - 1) out[outlen++] = ch[0]; col += w; } if (out) out[outlen] = '\0'; return row + 1; } /* 读入文件并建行表;0=文本预览可读,-1=非文本/读失败。 */ static int viewer_build(const char *path) { free(g_buf); free(g_line); free(g_rown); g_buf = NULL; g_line = NULL; g_rown = NULL; g_nline = 0; g_nrows = 0; g_buf_len = 0; g_fname[0] = '\0'; int fd = open(path, O_RDONLY); if (fd < 0) return -1; size_t cap = APP_READ_MAX + 1; g_buf = (char *)malloc(cap); if (!g_buf) { close(fd); return -1; } ssize_t n = read(fd, g_buf, APP_READ_MAX); close(fd); if (n <= 0) { free(g_buf); g_buf = NULL; return -1; } g_buf_len = (size_t)n; g_buf[g_buf_len] = '\0'; /* 非文本嗅探:前 512 字节出现 NUL 视为二进制 */ int lim = (int)(g_buf_len < 512 ? g_buf_len : 512); for (int i = 0; i < lim; i++) if (g_buf[i] == '\0') { free(g_buf); g_buf = NULL; return -1; } /* 文件名 */ const char *bn = fsx_basename(path); strncpy(g_fname, bn, FSX_MAX_NAME - 1); g_fname[FSX_MAX_NAME - 1] = '\0'; /* 切逻辑行(上限 VIEWER_MAX_LINES;超出截断,末行吸收剩余 + 附截断提示) */ int cap_l = VIEWER_MAX_LINES; g_line = (int *)malloc((size_t)cap_l * sizeof(int)); g_rown = (int *)malloc((size_t)cap_l * sizeof(int)); if (!g_line || !g_rown) { free(g_buf); free(g_line); free(g_rown); g_buf = NULL; g_line=NULL; g_rown=NULL; return -1; } g_nline = 0; g_line[g_nline++] = 0; int truncated = 0; for (size_t i = 0; i < g_buf_len; i++) { if (g_buf[i] == '\n') { if (g_nline < cap_l) g_line[g_nline++] = (int)i + 1; else { truncated = 1; break; } } } if (truncated) { g_line[g_nline - 1] = (int)g_buf_len; /* 让最后一行吸收剩余 */ } /* 折行累加 → 屏幕行下标 */ g_nrows = 0; for (int li = 0; li < g_nline; li++) { g_rown[li] = g_nrows; g_nrows += viewer_wrap_line(li, -1, NULL, 0); } return 0; } /* 宿主测试/入口兼容:读入 buffer 并解析行。 */ void viewer_load(const char *path) { viewer_build(path); } /* 内容区 render_cb:虚拟化画可见屏幕行。 */ static void viewer_render(meow_widget *w, const meow_theme *th, void *ctx) { (void)w; (void)ctx; if (!g_buf || g_nrows <= 0) { app_draw_empty(r_content, "NOT A TEXT FILE", g_fname[0] ? g_fname : ""); return; } int rowh = viewer_rowh(th); if (rowh < 1) rowh = 1; int vp = viewer_vp_h(); int top = g_vscroll / rowh; int bot = (g_vscroll + vp) / rowh + 1; if (top < 0) top = 0; if (bot > g_nrows) bot = g_nrows; char row[512]; for (int sr = top; sr < bot; sr++) { /* 二分找逻辑行 li:g_rown[li] <= sr 的最大 li */ int lo = 0, hi = g_nline - 1, li = 0; while (lo <= hi) { int mid = (lo + hi) / 2; if (g_rown[mid] <= sr) { li = mid; lo = mid + 1; } else { hi = mid - 1; } } int intra = sr - g_rown[li]; row[0] = '\0'; viewer_wrap_line(li, intra, row, (int)sizeof row); if (!row[0] && intra == 0) row[0] = ' ', row[1] = '\0'; int ry = r_content.y + (sr * rowh - g_vscroll); if (ry < r_content.y) continue; if (ry >= r_content.y + vp) break; meow_ui_text(r_content.x + APP_PAD, ry, row, th->font_body, th->onSurface); } /* 页脚:行数 + 文件名(含截断提示) */ char foot[128]; if (g_buf_len >= APP_READ_MAX) snprintf(foot, sizeof foot, ">256KB (truncated) %d row(s)", g_nrows); else snprintf(foot, sizeof foot, "%d row(s)", g_nrows); app_draw_footer(r_content, foot); } static int viewer_hit(meow_widget *w, int x, int y, void *ctx) { (void)w; (void)ctx; return x >= r_content.x && x < r_content.x + r_content.w && y >= r_content.y && y < r_content.y + r_content.h; } static void viewer_clamp_scroll(void) { const meow_theme *th = meow_theme_get(); int rowh = viewer_rowh(th); int content_h = g_nrows * rowh; int maxs = content_h - viewer_vp_h(); if (maxs < 0) maxs = 0; if (g_vscroll < 0) g_vscroll = 0; if (g_vscroll > maxs) g_vscroll = maxs; } int viewer_open_file(const char *path) { if (viewer_build(path) != 0) return -1; meow_view_page_push(ACT_VIEWER); return 0; } int viewer_is_active(void) { return g_vc.active; } void viewer_swipe(int dir, int dist) { if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; g_vscroll += del; viewer_clamp_scroll(); meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h); } void viewer_touch_down(int x, int y) { (void)x; g_vdrag = 0; g_vdrag_last_y = 0; if (!g_buf) return; if (meow_widget_hit(g_vcw, x, y)) { g_vdrag = 1; g_vdrag_last_y = y; } } void viewer_touch_move(int x, int y) { (void)x; if (!g_vdrag) return; int dy = y - g_vdrag_last_y; g_vdrag_last_y = y; if (dy == 0) return; g_vscroll -= dy; viewer_clamp_scroll(); meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h); } int viewer_on_tap(int x, int y) { if (g_tb) { int ah = meow_topappbar_hit(g_tb, x, y); if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; } } return 0; } /* ---- 页 view 生命周期 ---- */ static void viewer_page_create(meow_view *v, void *ctx) { (void)v; ((viewer_ctrl *)ctx)->view = v; r_content = app_content_rect(); meow_ui_activity_push(ACT_VIEWER); /* 让下面控件 owner=ACT_VIEWER */ g_tb = app_topbar_create("Preview", 1, NULL); g_vcw = meow_widget_custom_create(r_content.x, r_content.y, r_content.w, r_content.h, viewer_render, viewer_hit, NULL); meow_ui_activity_pop(); } static void viewer_page_enter(meow_view *v, void *ctx) { (void)v; ((viewer_ctrl *)ctx)->active = 1; g_vscroll = 0; if (g_tb) meow_topappbar_set_title(g_tb, g_fname[0] ? g_fname : "Preview"); const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); meow_ui_paint_full(); } static void viewer_page_leave(meow_view *v, void *ctx) { (void)v; ((viewer_ctrl *)ctx)->active = 0; } static void viewer_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; if (g_vcw) { meow_widget_free(g_vcw); g_vcw = NULL; } if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; } free(g_buf); free(g_line); free(g_rown); g_buf = NULL; g_line = NULL; g_rown = NULL; } meow_view *viewer_view_create(void) { static const meow_view_hooks h = { .on_create = viewer_page_create, .on_enter = viewer_page_enter, .on_leave = viewer_page_leave, .on_destroy = viewer_page_destroy, }; return meow_view_create(ACT_VIEWER, MEOW_VIEW_ACTIVITY, &h, &g_vc); }