- launcher(453): app_launcher.c g_launch_fail_reason+launch_fail()统一出口(5失败点 fork OOM/exec/no such file/missing dep/execpipe)+main_menu.c launch_fail_toast(缺依赖/CANNOT START: name(reason))+toast256 - launcher(457): bg_page.c bgm_enumerate跳过term_at_ms(杀后当下消失)+不可杀行按钮置灰+row_tap/top_action返回1; subpage.c 消warning - reader(454): reader_on_swipe LEFT/RIGHT→返回(先关overlay否则page_pop回书架)+app.c书架根LEFT→exit - reader(458瘦身A+B): app.json fonts full→common(省13MB atlas)+reader.c RD_PAGE_CACHE 3→2+g_anim_old懒分配/anim_done释放+on_enter/on_leave页位图离开释放重建 - reader(459导航): reader_open返回int+shelf点书push ACT_READER(阅读页接入view栈,on_enter/on_leave驱动) — 使454左缘返回+458瘦身B真正生效 - files(455): viewer.c viewer_cp_len+按码位折行(txt中文不冲出右缘) 0warning;宿主测试全过;files二进制已热更
304 lines
11 KiB
C
304 lines
11 KiB
C
/* 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 <fcntl.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
|
||
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; }
|
||
|
||
/* UTF-8 码位字节长(1..4):s[bi] 起、串内有效字节数 lim。
|
||
* 首字节非法/续字节非法/序列不完整 → 保守按 1(单字节);保证折行绝不断在多字节码位中间,
|
||
* 也保证宽测量(meow_ui_text_width 按码位测)对中文/混合文本正确。 */
|
||
static int viewer_cp_len(const unsigned char *s, int bi, int lim)
|
||
{
|
||
unsigned char c = s[bi];
|
||
int n;
|
||
if (c < 0x80) return 1;
|
||
else if ((c & 0xE0) == 0xC0) n = 2;
|
||
else if ((c & 0xF0) == 0xE0) n = 3;
|
||
else if ((c & 0xF8) == 0xF0) n = 4;
|
||
else return 1;
|
||
if (lim - bi < n) return 1;
|
||
for (int i = 1; i < n; i++)
|
||
if ((s[bi + i] & 0xC0) != 0x80) return 1;
|
||
return n;
|
||
}
|
||
|
||
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。
|
||
* 按「UTF-8 码位」逐字累加显示宽度(meow_ui_text_width 按码位测>0),到内容宽 colw 即断行;
|
||
* 多字节(CJK)整字不断开。 */
|
||
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;
|
||
const unsigned char *s = (const unsigned char *)g_buf + start;
|
||
for (int bi = 0; bi < len; ) {
|
||
int clen = viewer_cp_len(s, bi, len);
|
||
if (clen == 1 && s[bi] == '\r') { bi++; continue; }
|
||
char cp[5]; cp[clen] = '\0';
|
||
memcpy(cp, s + bi, (size_t)clen); bi += clen;
|
||
int w = meow_ui_text_width(cp, 20);
|
||
if (col + w > colw && col > 0) { row++; col = 0; }
|
||
if (row == intra && out && outlen + clen < outsz) { memcpy(out + outlen, cp, (size_t)clen); outlen += clen; }
|
||
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);
|
||
}
|