Files
AskaEth ddc8d35eb9 fix: M6.41批 — 启动失败原因透传 + 阅读器左缘返回/阅读页接入view栈 + 后台管理按钮点不动 + 阅读器瘦身(fonts common+页位图释放) + files txt换行
- 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二进制已热更
2026-09-04 19:43:28 +08:00

304 lines
11 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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);
}