feat(files): 新增外置系统文件管理器 os.meow.files (agent-183) — 目录浏览(虚拟化列表/空态)+文本预览(≤256KB截断)+删除/重命名/新建/属性(BottomSheet+IME);路径写操作限 TF 卡内(拒 .. 越界);多文件结构(app.c+ui/{browser,viewer}.c+ui/common/chrome.c+logic/fsx.c) 按批次②范本

files md5 6f6b0a6d; 0 warning; 宿主 test_fsx 全过(路径安全/格式化/排序/虚拟化/写操作/文本嗅探); qemu 离屏冒烟通过
SDK 缺口记录: listview 仅单行 label(本 App 用 custom 画布自绘行); common 档字库中文缺字(未声明 fonts:full)
真机待验证: 列表渲染/滚动手感/IME 重命名/弹窗交互
This commit is contained in:
MeowOS committed 2026-08-31 04:03:56 +08:00
1 parent b5038f8673
commit c187b7bce3
12 files changed
+1741

No files matched your search

+504
View File
@@ -0,0 +1,504 @@
/* ui/browser.c — 目录浏览页(ACT_BROWSER,根页)view + 控制器。
*
* 职责:
* - 持有目录态(g_cur 当前目录 / g_entries 缓存快照 / g_err 空态码 / g_scroll);
* - 内容区用 meow_widget_custom_create 自绘「虚拟化列表」:只画可见行(fsx_visible_range),
* 行=图标(目录/文件)+名称+大小/时间+右侧 ⋯ 操作钮;不一次全读进内存(entries 有界缓存);
* - 空态/错误(无卡/空目录/权限)由 render_cb 内 app_draw_empty 画;
* - 触摸:行 body=进目录/开预览;右侧操作钮或长按 → BottomSheet(删除/重命名/新建/属性);
* - 滑动:LEFT=返回上级(App 层处理);UP/DOWN=列表滚动(本页);
* - 写操作只经 logic(fsx_*),且在卡内校验后执行。
*
* 颜色全部读 meow_theme_get() 语义色(§8.1),不硬编码。
*/
#include "files_internal.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct { meow_view *view; int active; } browser_ctrl;
static browser_ctrl g_bc;
static fsx_entry g_entries[FSX_MAX_ENTRIES];
static int g_n; /* 当前目录项数 */
static char g_cur[FSX_MAX_PATH];
static int g_err; /* fsx_scan_dir 返回(0=ok,<0=空态码) */
static int g_scroll; /* 列表滚动(px) */
static int g_sel; /* 当前选中行(-1=无) */
static int g_press_row; /* 按下行(-1=无);长按检测 */
static uint64_t g_press_ms;
static int g_press_tapped; /* 长按已消费本按 */
static int g_press_delta; /* 按下后累计位移(>阈值取消长按) */
static int g_dragging; /* 本次触摸起点是否在列表(拖动滚列表) */
static int g_drag_last_y; /* 拖拽上一帧 y */
static meow_widget *g_tb; /* TopAppBar(chrome) */
static meow_widget *g_list; /* 内容区虚拟化列表(custom widget) */
static app_rect_t r_content;
static char g_title[FSX_MAX_PATH];
/* 预览/操作辅助(前向声明) */
static void browser_open_sheet(int row);
static void browser_enter_dir(const char *path);
static void browser_open_file(const char *path);
static void browser_rename_entry(const fsx_entry *e);
static void browser_delete_entry(const fsx_entry *e);
static void browser_props_entry(const fsx_entry *e);
static void browser_new_folder_in(const char *dirpath);
static void browser_report_ret(int r);
/* 本轮按下状态复位(每次 on_tap / on_touch_down 前清)。 */
static void browser_drag_reset(void)
{
g_press_row = -1;
g_press_tapped = 0;
g_press_delta = 0;
g_dragging = 0;
g_drag_last_y = 0;
}
/* ---- 通用文本绘制:宽度限 maxw 内截断(超出加 "...")---- */
static void browser_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col)
{
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[FSX_MAX_NAME + 4];
int i;
for (i = 0; s[i] && i < FSX_MAX_NAME - 4; i++) {
tmp[i] = s[i];
tmp[i + 1] = '\0';
if (meow_ui_text_width(tmp, size) > maxw - dw) break;
}
if (i >= FSX_MAX_NAME - 4) i = FSX_MAX_NAME - 4;
memcpy(tmp + i, dots, 4);
meow_ui_text(x, y, tmp, size, col);
}
/* ---- 列表行图标:目录=filled 矩形+页签;文件=空心矩形+折角 ---- */
static void browser_draw_icon(const meow_theme *th, int x, int y, int is_dir, int sel)
{
uint32_t col = sel ? 0xFFFFFF : (is_dir ? th->primary : th->onSurfaceVariant);
if (is_dir) {
meow_ui_fill_rect(x, y + 6, 26, 18, col); /* 主体 */
meow_ui_fill_rect(x, y + 2, 12, 6, col); /* 页签 */
} else {
meow_ui_draw_rect(x + 4, y + 4, 18, 22, col); /* 页体 */
meow_ui_fill_rect(x + 4, y + 4, 7, 4, col); /* 文件名横条 */
}
}
/* 行区视口高(底部留页脚,行不压到页脚)。 */
static int browser_vp_h(void) { return r_content.h - APP_FOOTER_H; }
/* ---- 列表 render_cb:虚拟化画可见行 + 空态 + 页脚 ---- */
static void browser_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)w; (void)ctx;
/* 空态/错误 */
if (g_err != 0) {
if (g_err == FSX_ERR_NOENT)
app_draw_empty(r_content, "NO STORAGE CARD", "insert a TF card / mount sd_0");
else if (g_err == FSX_ERR_EACCES)
app_draw_empty(r_content, "ACCESS DENIED", "cannot read this folder");
else
app_draw_empty(r_content, "CANNOT OPEN", "unexpected error");
return;
}
if (g_n == 0) {
app_draw_empty(r_content, "EMPTY FOLDER", g_cur);
app_draw_footer(r_content, "0 items");
return;
}
int vp = browser_vp_h();
int first, last;
fsx_visible_range(g_n, APP_ROW_H, g_scroll, vp, &first, &last);
int thh = meow_ui_text_height(th->font_body);
int namew = r_content.w - APP_PAD - APP_ICON_W - APP_PAD - APP_OP_W - APP_PAD; /* 名称区 */
char size[32], tm[32];
for (int i = first; i < last; i++) {
fsx_entry *e = &g_entries[i];
int ry = r_content.y + (i * APP_ROW_H - g_scroll);
if (ry + APP_ROW_H <= r_content.y) continue;
if (ry >= r_content.y + vp) break;
int hilite = (i == g_sel || i == g_press_row);
if (hilite) meow_ui_fill_rect(r_content.x, ry, r_content.w, APP_ROW_H, th->primary);
browser_draw_icon(th, r_content.x + APP_PAD, ry + (APP_ROW_H - 24) / 2, e->is_dir, hilite);
uint32_t namecol = hilite ? 0xFFFFFF : th->onSurface;
uint32_t subcol = hilite ? 0xFFFFFF : th->dim;
browser_text_trunc(r_content.x + APP_PAD + APP_ICON_W + APP_PAD, ry + 6,
e->name, namew, th->font_body, namecol);
if (e->is_dir) {
fsx_fmt_time(e->mtime, tm, sizeof tm);
snprintf(size, sizeof size, "Folder %s", tm);
} else {
fsx_fmt_size(e->size, size, sizeof size);
fsx_fmt_time(e->mtime, tm, sizeof tm);
snprintf(size, sizeof size, "%s %s", size, tm);
}
browser_text_trunc(r_content.x + APP_PAD + APP_ICON_W + APP_PAD, ry + 6 + thh + 2,
size, namew, 20, subcol);
/* 右侧 ⋯ 操作钮(三个点) */
int ox = r_content.x + r_content.w - APP_OP_W / 2 - 1;
int oy = ry + APP_ROW_H / 2 - 8;
for (int d = 0; d < 3; d++)
meow_ui_fill_rect(ox, oy + d * 6, 4, 4, hilite ? 0xFFFFFF : th->onSurfaceVariant);
/* 分隔线 */
int sy = ry + APP_ROW_H - 1;
if (sy >= r_content.y && sy < r_content.y + r_content.h)
meow_ui_fill_rect(r_content.x, sy, r_content.w, 1, th->outlineVariant);
}
char foot[96];
if (g_n >= FSX_MAX_ENTRIES)
snprintf(foot, sizeof foot, "%d items (showing first %d)", g_n, FSX_MAX_ENTRIES);
else
snprintf(foot, sizeof foot, "%d items %s", g_n, g_cur);
app_draw_footer(r_content, foot);
}
/* 列表 hit_cb:落点是否在内容区(行判定在 on_tap 里做)。 */
static int browser_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 browser_update_title(void)
{
const char *name = fsx_basename(g_cur);
if (fsx_is_root(g_cur)) snprintf(g_title, sizeof g_title, "SD Card");
else snprintf(g_title, sizeof g_title, "%s", name);
if (g_title[0] == '\0') snprintf(g_title, sizeof g_title, "Files");
if (g_tb) meow_topappbar_set_title(g_tb, g_title);
}
void browser_reload(void)
{
int r = fsx_scan_dir(g_cur, g_entries, FSX_MAX_ENTRIES, &g_n);
g_err = (r < 0) ? r : 0;
if (g_err != 0) g_n = 0;
g_scroll = 0;
g_sel = -1;
browser_update_title();
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
void browser_enter_dir(const char *path)
{
if (!path) return;
strncpy(g_cur, path, sizeof g_cur - 1);
g_cur[sizeof g_cur - 1] = '\0';
browser_reload();
}
int browser_go_up(void)
{
if (fsx_is_root(g_cur)) return 0;
char up[FSX_MAX_PATH];
if (fsx_dirname(g_cur, up, sizeof up) != 0) return 0;
browser_enter_dir(up);
return 1;
}
int browser_at_root(void) { return fsx_is_root(g_cur); }
int browser_is_active(void) { return g_bc.active; }
const char *browser_current_dir(void) { return g_cur; }
int browser_entry_count(void) { return g_n; }
/* ---- 行操作 ---- */
static void browser_report_ret(int r)
{
if (r == 0) return;
const char *msg = strerror(-r);
meow_dialog *d = meow_dialog_create(MEOW_DLG_INFO, "Action failed");
const char *msgs[1];
msgs[0] = msg ? msg : "error";
meow_dialog_set_items(d, msgs, 1, -1);
meow_dialog_show(d);
meow_dialog_free(d);
}
static void browser_rename_entry(const fsx_entry *e)
{
meow_ime_ctx ime;
memset(&ime, 0, sizeof ime);
ime.mode = MEOW_IME_ASCII;
strncpy(ime.buf, e->name, sizeof ime.buf - 1);
ime.maxlen = 120;
meow_ui_text_input(&ime);
char nb[FSX_MAX_NAME];
if (fsx_sanitize_name(ime.buf, nb, sizeof nb) == 0 && strcmp(ime.buf, e->name) != 0) {
int r = fsx_rename_into(e->path, nb);
browser_report_ret(r);
}
}
static void browser_delete_entry(const fsx_entry *e)
{
char m[192];
snprintf(m, sizeof m, "Really delete \"%s\"%s?",
e->name, e->is_dir ? " (folder must be empty)" : "");
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Delete");
const char *msgs[1];
msgs[0] = m;
meow_dialog_set_items(d, msgs, 1, -1);
int yes = meow_dialog_confirm(d);
meow_dialog_free(d);
if (yes) { int r = fsx_delete(e->path); browser_report_ret(r); }
}
static void browser_props_entry(const fsx_entry *e)
{
fsx_entry st;
if (fsx_stat_path(e->path, &st) != 0) return;
char size[32], tm[32], l1[64], l2[64], l3[64];
fsx_fmt_size(st.size, size, sizeof size);
fsx_fmt_time(st.mtime, tm, sizeof tm);
snprintf(l1, sizeof l1, "Type: %s", st.is_dir ? "Directory" : "File");
snprintf(l2, sizeof l2, "Size: %s", st.is_dir ? "-" : size);
snprintf(l3, sizeof l3, "Modified: %s", tm);
const char *msgs[3];
msgs[0] = l1; msgs[1] = l2; msgs[2] = l3;
meow_dialog *d = meow_dialog_create(MEOW_DLG_INFO, st.name);
meow_dialog_set_items(d, msgs, 3, -1);
meow_dialog_show(d);
meow_dialog_free(d);
}
static void browser_new_folder_in(const char *dirpath)
{
meow_ime_ctx ime;
memset(&ime, 0, sizeof ime);
ime.mode = MEOW_IME_ASCII;
ime.maxlen = 120;
meow_ui_text_input(&ime);
char nb[FSX_MAX_NAME];
if (fsx_sanitize_name(ime.buf, nb, sizeof nb) == 0) {
int r = fsx_mkdir(dirpath, nb);
browser_report_ret(r);
if (r == 0 && browser_is_active()) browser_reload();
}
}
static void browser_new_folder(void) /* 当前目录下新建 */
{
browser_new_folder_in(g_cur);
}
/* BottomSheet:按行类型建不同选项清单。 */
static void browser_open_sheet(int row)
{
fsx_entry *e = &g_entries[row];
char title[128];
snprintf(title, sizeof title, "%s: %s", e->is_dir ? "Folder" : "File", e->name);
meow_sheet *s = meow_sheet_create(title, 0);
if (e->is_dir) {
meow_sheet_add_item(s, "Open");
meow_sheet_add_item(s, "New Folder");
meow_sheet_add_item(s, "Rename");
meow_sheet_add_item(s, "Delete");
meow_sheet_add_item(s, "Properties");
} else {
meow_sheet_add_item(s, "Open");
meow_sheet_add_item(s, "Rename");
meow_sheet_add_item(s, "Delete");
meow_sheet_add_item(s, "Properties");
}
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op < 0) return;
if (op == 0) { /* Open */
if (e->is_dir) browser_enter_dir(e->path);
else browser_open_file(e->path);
} else if (e->is_dir) {
if (op == 1) browser_new_folder_in(e->path);
else if (op == 2) browser_rename_entry(e);
else if (op == 3) browser_delete_entry(e);
else if (op == 4) browser_props_entry(e);
} else {
if (op == 1) browser_rename_entry(e);
else if (op == 2) browser_delete_entry(e);
else if (op == 3) browser_props_entry(e);
}
if (browser_is_active()) browser_reload(); /* 写操作后重扫(Open 文件时 viewer 前台则跳过) */
}
/* 打开文件:文本→预览页;否则提示。 */
static void browser_open_file(const char *path)
{
if (fsx_is_text_path(path)) {
if (viewer_open_file(path) == 0) return; /* 已 push 预览页 */
browser_report_ret(-EIO);
return;
}
meow_dialog *d = meow_dialog_create(MEOW_DLG_INFO, "Preview");
const char *msgs[1];
msgs[0] = "This is not a text file.\nNo preview available.";
meow_dialog_set_items(d, msgs, 1, -1);
meow_dialog_show(d);
meow_dialog_free(d);
}
/* ---- 输入分发 ---- */
int browser_on_tap(int x, int y)
{
if (g_press_tapped) { browser_drag_reset(); return 1; }
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { browser_drag_reset(); browser_go_up(); return 1; }
if (ah >= 0) { browser_drag_reset(); browser_new_folder(); return 1; } /* NEW */
}
if (g_err != 0 || g_n == 0) return 0;
if (meow_widget_hit(g_list, x, y)) {
int op = (x >= r_content.x + r_content.w - APP_OP_W);
int row = (y - r_content.y + g_scroll) / APP_ROW_H;
if (row < 0 || row >= g_n) { browser_drag_reset(); return 1; }
g_sel = row;
if (op) browser_open_sheet(row);
else {
fsx_entry *e = &g_entries[row];
if (e->is_dir) browser_enter_dir(e->path);
else browser_open_file(e->path);
}
browser_drag_reset();
return 1;
}
return 0;
}
/* 触摸拖拽滚动(滚列表不滚页;topbar 不拖)。 */
void browser_touch_down(int x, int y)
{
browser_drag_reset();
if (g_err != 0 || g_n == 0) return;
if (meow_widget_hit(g_list, x, y)) {
int row = (y - r_content.y + g_scroll) / APP_ROW_H;
if (row >= 0 && row < g_n) {
g_press_row = row;
g_press_ms = meow_ui_time_ms();
}
g_dragging = 1;
g_drag_last_y = y;
meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h);
}
}
void browser_touch_move(int x, int y)
{
(void)x;
if (!g_dragging) return;
if (g_press_row >= 0) { /* 拖动取消长按 */
int d = y - g_drag_last_y;
if (d < 0) d = -d;
if (d > 0) g_press_delta += d;
if (g_press_delta > 10) g_press_row = -1;
}
int dy = y - g_drag_last_y;
g_drag_last_y = y;
if (dy == 0) return;
int ns = g_scroll - dy;
int ms = fsx_max_scroll(g_n, APP_ROW_H, browser_vp_h());
if (ns < 0) ns = 0;
if (ns > ms) ns = ms;
if (ns != g_scroll) {
g_scroll = ns;
meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h);
}
}
void browser_swipe(int dir, int dist)
{
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_err != 0 || g_n == 0) return;
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 上滑→显示更低内容 */
int ns = g_scroll + del;
int ms = fsx_max_scroll(g_n, APP_ROW_H, browser_vp_h());
if (ns < 0) ns = 0;
if (ns > ms) ns = ms;
if (ns != g_scroll) {
g_scroll = ns;
meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h);
}
}
/* on_tick:长按(≥500ms 未拖动)→ 打开该行 BottomSheet。 */
void browser_tick(uint64_t now)
{
if (g_press_row >= 0 && !g_press_tapped && now - g_press_ms >= 500) {
g_press_tapped = 1;
int row = g_press_row;
g_press_row = -1;
browser_open_sheet(row); /* 内部:写操作后自行 reload */
}
}
/* ---- 页 view 生命周期 ---- */
static void browser_page_create(meow_view *v, void *ctx)
{
(void)v;
((browser_ctrl *)ctx)->view = v;
r_content = app_content_rect();
strncpy(g_cur, FSX_SD_ROOT, sizeof g_cur - 1);
g_cur[sizeof g_cur - 1] = '\0';
g_tb = app_topbar_create("Files", 1, "NEW");
g_list = meow_widget_custom_create(r_content.x, r_content.y, r_content.w, r_content.h,
browser_render, browser_hit, NULL);
g_sel = -1;
g_press_row = -1;
browser_reload();
}
static void browser_page_enter(meow_view *v, void *ctx)
{
(void)v;
((browser_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
static void browser_page_leave(meow_view *v, void *ctx)
{
(void)v;
((browser_ctrl *)ctx)->active = 0;
}
static void browser_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *browser_view_create(void)
{
static const meow_view_hooks h = {
.on_create = browser_page_create,
.on_enter = browser_page_enter,
.on_leave = browser_page_leave,
.on_destroy = browser_page_destroy,
};
return meow_view_create(ACT_BROWSER, MEOW_VIEW_ACTIVITY, &h, &g_bc);
}
+57
View File
@@ -0,0 +1,57 @@
/* ui/common/chrome.c — files 应用共享 chrome(TopAppBar 建置 / 几何 / 空态&页脚绘制)。
*
* 与 hello_app/ui/common/chrome.c 同理:把「页面 chrome 固定不随内容重建」分出去;本模块只
* 提供几何计算 + topbar 建置 + 非控件直绘辅助(空态/页脚),供 browser/viewer 两页复用。
* 绘制全部读 meow_theme_get() 语义色(§8.1),不硬编码配色;topbar 由 meow_topappbar 建,
* 自动注册进绘制物注册表(§6.2),owner=创建时 active activity。
*/
#include "files_internal.h"
int app_statusbar(void)
{
int sb = meow_ui_statusbar_height();
return sb < 0 ? 0 : sb;
}
app_rect_t app_topbar_rect(void)
{
int sb = app_statusbar();
return AR(0, sb, meow_ui_width(), APP_TOPBAR_H);
}
app_rect_t app_content_rect(void)
{
int sb = app_statusbar();
int y = sb + APP_TOPBAR_H;
int h = meow_ui_height() - y;
if (h < 0) h = 0;
return AR(0, y, meow_ui_width(), h);
}
meow_widget *app_topbar_create(const char *title, int back, const char *action_label)
{
app_rect_t r = app_topbar_rect();
meow_widget *tb = meow_topappbar_create(r.x, r.y, r.w, r.h, title);
meow_topappbar_set_back(tb, back);
if (action_label) meow_topappbar_add_action(tb, action_label);
meow_widget_set_z(tb, 999); /* chrome 置顶(§6.2:> 一切自动 z,区域重画最后画它) */
return tb;
}
/* 空态/错误:居中两行(kind 大字 + what 小字)。kind=状态标题,what=说明/路径。 */
void app_draw_empty(app_rect_t r, const char *kind, const char *what)
{
const meow_theme *th = meow_theme_get();
int cy = r.y + r.h / 2;
meow_ui_text_center(r.x + r.w / 2, cy - 34, kind ? kind : "", 28, th->primary);
if (what && what[0])
meow_ui_text_center(r.x + r.w / 2, cy + 4, what, 20, th->dim);
}
/* 底部信息(右对齐 dim):项数/路径/截断提示。 */
void app_draw_footer(app_rect_t r, const char *text)
{
const meow_theme *th = meow_theme_get();
if (!text || !text[0]) return;
meow_ui_text_right(r.x + r.w - APP_PAD, r.y + r.h - 26, text, 20, th->dim);
}
+281
View File
@@ -0,0 +1,281 @@
/* 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; }
/* 折行:返回逻辑行 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);
}