c/app/notepad.c · 34601 字节 · 下载原文
/* notepad.c —— 常用小程序④:**记事本**(第 4 层:`ui_*` 图形界面)
* notepad.c -- Everyday utility #4: a **notepad** (layer 4: the `ui_*` graphical interface)
*
* 一扇窗口里的纯文本编辑器:**打字 / 退格 / 回车换行 / 光标可见**,`Esc` **保存并退出**,
* A plain text editor in one window: **type, backspace, Enter for a new line, a visible caret**, `Esc`
* 下次运行把上次的内容读回来。同一份源码在手机上(点屏幕 + 软键盘)与桌面上(物理键盘)都跑。
* **saves and quits**, and the next run reads the last session back. One source file runs on the phone
* (touch + soft keyboard) and on the desktop (a physical keyboard) alike.
*
* ## 怎么编辑
* ## How to edit
*
* · 可打印 ASCII(32~126)插到**光标处**;`退格`(8) 删前一个字符;`回车`(13) 换行。
* · Printable ASCII (32-126) goes in **at the caret**; Backspace (8) deletes one character; Enter (13)
* starts a new line.
* · `←`/`→` 挪光标;`Esc`(27) = **保存并退出**。
* · Left/Right move the caret; `Esc` (27) = **save and quit**.
* · 空闲 10 秒也退出,且**走同一条收尾**(见下面第 ⑤ 条)。
* · Ten idle seconds quits too, through **the very same exit path** (pitfall 5 below).
*
* ## 存到哪
* ## Where it saves
*
* **相对路径 `note.txt`** —— 手机上沙箱根 = 工作区(绝对路径会被拒)。
* **The relative path `note.txt`** -- on the phone the sandbox root is the workspace, and absolute
* 桌面上落在**源文件所在目录**(`vmlcli` 按 `WorkDir` 解析)。
* paths are rejected; on the desktop it lands in the **directory of the source file** (`vmlcli`
* derives `WorkDir`).
* 跑法(桌面脚手架,秒级):
* How to run (desktop scaffolding, seconds):
* dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll Examples/c/app/notepad.c --vml-home third_party/vml
* 手机上在命令行页里敲:
* On the phone, type this in the command-line page:
* vml run examples/c/app/notepad.c
*
* ## 踩过的坑(实测)
* ## Pitfalls (measured in this repo)
*
* ① **打开模式写反 = 把文件清空**:`#110` 的 `mode=1` 是 `FileMode.Create`(**先截断再写**)。
* ① **Swapping the open modes wipes the file**: `#110` with `mode=1` means `FileMode.Create`, which
* **truncates first**.
* 所以"想读内容却传了 1"读到的是**空串**。
* So "meant to read, passed 1" hands back an **empty string**.
* 读=0、写=1,两个常量**各写一次、别共用**。
* Read = 0, write = 1, each written once and never shared.
*
* ② **正文不能"一个字节画一格"**:UTF-8 里一个汉字是 3 个字节。
* ② **The body must not be drawn "one byte per cell"**: one CJK glyph is 3 bytes in UTF-8.
* 实测:逐字节画出来是 `τ □ □ Σ …` 这样一排盒子;**按码点切**才对。
* Measured: drawing byte by byte came out as a row of `τ □ □ Σ` boxes; cutting by **code point**
* (`utf8_len` + `cell_width`) is what works.
*
* ③ **"一格放得下吗"只有一处判据**:画正文与定位光标若各写一套换行规则,就会出现
* ③ **"Does it fit in a cell" has exactly one judge**: two copies of the wrap rule, one for drawing
* "光标停在行尾、字却掉到下一行行首"。
* and one for the caret, produce "the caret sits at the line end while the character lands at the
* start of the next line".
* 共用 `cell_fit`。
* Both go through `cell_fit`.
*
* ④ **回车就是缓冲里的字节 `10`**:`VML_KEY_ENTER` 要自己变成 `10` 再存盘。
* ④ **Enter is nothing but the byte 10 in the buffer**: `VML_KEY_ENTER` has to become a `10` before
* the save.
* 忘了这一步,屏幕上看着换行了、存到文件里的却是一行(下次读回来全挤在一起)。
* Skip it and the screen breaks the line while the file keeps one long line (the next run reads it
* back all run together).
*
* ⑤ **收尾只有一处**:`Esc` / 空闲 10 秒 / 关窗三条路都从循环**之后**那一段出去。
* ⑤ **A single exit**: `Esc`, ten idle seconds and the window's close arrow all leave through the one
* place **after** the loop.
* 把保存写进 `Esc` 那一支 = 另外两条路**悄悄丢内容**。
* Putting the save inside the `Esc` arm would silently lose the text on the other two.
* 本仓 `E169`「谁发起的,就要能被同一处停掉」的同族。
* It is the same family as this repo's `E169`, "whoever starts it must be stoppable in one place".
*
* ⑥ **`ui_2d_text` 的 `y` 是基线,不是"字盒的顶"**:墨迹落在 `y` 的**上方**。
* ⑥ **`ui_2d_text`'s `y` is the baseline, not the top of the glyph box**: the ink sits **above** it.
* 实测(字号 16、`y=58`):拉丁字母的墨迹在 48..57、汉字在 47..59。
* Measured (size 16, `y=58`): the Latin ink is at 48..57 and the CJK ink at 47..59.
* 按"y 是盒顶"去画光标,光标就落在**自己那一行的下面**(看着像"光标跳到了下一行")。
* Draw the caret assuming "y is the top" and it lands **below its own line** (it reads as if the
* caret had jumped a line -- measured).
* 纸面也得往上多让一点,不然第一行会顶着框;见 `draw` 里的 `ink`。
* The sheet needs that headroom too, or the first line touches the frame; see `ink` in `draw`.
*
* ⑦ **读进来的 `\r` 要清掉**:本程序写的是 LF。
* ⑦ **Strip the `\r` on the way in**: this program writes LF.
* 但**别人的**文件(Windows 记事本、Git 的 `core.autocrlf`)是 CRLF。
* But files from **someone else** (Windows Notepad, Git's `core.autocrlf`) are CRLF.
* `\r` **看不见,却按"一个字"占掉一格**:实测 CRLF 文件被算成 "18 字"、原样存回 20 字节。
* A `\r` **is invisible yet takes one cell**: measured, a CRLF file counts as "18 characters" and
* saves back as 20 bytes.
* 清掉后是 "16 字" / 18 字节;`ab\r\ncd` 上光标停在 `ab` 右边**一格空位**(见 `strip_cr`)。
* After stripping it is "16 characters" / 18 bytes; on `ab\r\ncd` the caret sits **one empty cell**
* right of `ab` (see `strip_cr`).
*
* ⑧ **`VML_KEY_LEFT/UP/RIGHT/DOWN` = 37/38/39/40,全都落在"可打印 ASCII 32..126"里**。
* ⑧ **`VML_KEY_LEFT/UP/RIGHT/DOWN` are 37/38/39/40, all inside "printable ASCII 32..126".**
* 所以**先判可打印**就会把方向键当成字符插进去:实测连按三下左箭头,正文里多了 3 个 `%`。
* So testing the printable range first types the arrow key in as a character: measured, three Left
* presses left three `%` in the text.
* 而光标一步没动 —— 判 `VML_MSG_KEYDOWN` 时**特殊键一律排在可打印区间之前**。
* The caret did not move at all -- so when handling `VML_MSG_KEYDOWN`, **special keys always come
* before the printable range**.
*
* ## 界面文字跟着宿主语言走
* ## The on-screen text follows the host language
*
* `ui_scr_get_language()`(与 `demo_ui.c`/`calendar.c` 同一套做法)。
* `ui_scr_get_language()`, exactly as `demo_ui.c`/`calendar.c` do it.
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
#include <file.h> /* fopen 家族(真签名 `int mode`)—— 本文件不再自己包汇编 */
#define IDLE_EXIT_MS 10000 /* 空闲这么久就自己退出(例程要能被自动验收)*/
/* Idle this long and it quits (samples must be automatically verifiable). */
#define NOTE_CAP 2048 /* 正文缓冲容量(字节)*/
/* Text buffer capacity (bytes) */
#define NOTE_FILE "note.txt" /* ⚠ 相对路径 */
/* ⚠ A relative path */
/* 配色(真彩 AARRGGBB,第 4 层没有索引色)*/
/* Colours (true colour AARRGGBB; layer 4 has no palette) */
#define C_BG 0xFF101018 /* 窗口底 */
/* window background */
#define C_PAPER 0xFF1A1A24 /* 正文"纸"面 */
/* the sheet the text sits on */
#define C_TITLE 0xFFE8E8F0
#define C_TEXT 0xFFD8DCE8
#define C_DIM 0xFF9AA0B0
#define C_CUR 0xFFFFC64A /* 光标(黄色竖条)*/
/* the caret (a yellow bar) */
#define C_HINT 0xFF7A8090
/* ⚠ 这里原先有一层手写的 `sys_*` 包装(`asm("SYSCALL #110…")` + 全局 `_sysret` 中转)。
* 2026-10-07 起改调库:`#include <file.h>` 带来的 `fopen`/`fread`/`fwrite`/`fclose`
* **返回值就是 syscall 的返回值**,不再需要 `_sysret` 这个中转站。
* The hand-rolled `sys_*` wrappers are gone: the library functions (via `#include <file.h>`) return the
* syscall result directly, so the `_sysret` relay is unnecessary. */
/* ── 正文模型:**一维扁平缓冲** ─────────────────────────────────────── */
/* -- The text model: **one flat buffer** --
* ⚠ 不用二维数组、不用"指针数组"、不取数组元素的地址(`&arr[i]`)—— 这几样在本仓的多语言
* ⚠ No 2-D arrays, no arrays of pointers, no address-of on an array element (`&arr[i]`): those three are
* 前端之间最不稳(例程是要照着移植到 22 门语言的),一律一维 + 扁平下标。
* the least portable forms across this repo's front ends (the samples get ported to 22 languages), so
* everything is one-dimensional with flat indexes. */
char nbuf[NOTE_CAP]; /* 正文;**不保证以 0 结尾**(长度看 `blen`)*/
/* The text; **not guaranteed NUL-terminated** (the length is `blen`). */
int blen = 0; /* 已用字节数 */
/* bytes in use */
int tcur = 0; /* 光标:字节下标 0..blen */
/* caret: a byte index 0..blen */
int tloaded = 0; /* 启动时有没有读到文件(1=读到了)*/
/* whether the file was read at startup */
/* ── 版式(**一律从宿主给的尺寸算**,别写死坐标)───────────────────── */
/* -- Layout (**always computed from the host size**, never hard-coded) -- */
static int SW;
static int SH;
static int lang; /* 0 = 中文,1 = English */
/* 0 = Chinese, 1 = English */
static int g_pad; /* 页边距 */
/* page margin */
static int g_fs; /* 正文字号 */
/* body font size */
static int g_cw; /* **一格的宽**(不是"字体量的宽")*/
/* the **cell width** (not a measured glyph width) */
static int g_lh; /* 一行的高 */
/* line height */
static int g_x0; /* 正文区左上角 */
/* body origin */
static int g_y0;
static int g_cols; /* 每行几格 */
/* cells per line */
static int g_rows; /* 一屏几行 */
/* lines per screen */
static int g_top; /* 滚到第几个显示行(**光标的显示行**决定它)*/
/* first visible display row */
/* 整数 → 十进制串(`ui_2d_text` 只收字符串,第 4 层没有 `print_int` 那种东西)
* Integer to a decimal string (`ui_2d_text` takes strings only; layer 4 has no `print_int`).
* ⚠ 返回的是**同一个静态缓冲**:别在一个表达式里连着用两次(那要用两次就先 `strcat` 到自己串上)。
* ⚠ It returns **one static buffer**: never use it twice inside a single expression. */
static char numbuf[16];
static char *numstr(int v) {
itoa(v, numbuf);
return numbuf;
}
/* 这个字节起头是几个字节的 UTF-8 序列(认不出按 1 算)/ How many bytes the UTF-8 sequence starting
* here is (unrecognized counts as 1). */
static int utf8_len(int lead) {
if (lead < 0x80) return 1;
if ((lead & 0xE0) == 0xC0) return 2;
if ((lead & 0xF0) == 0xE0) return 3;
if ((lead & 0xF8) == 0xF0) return 4;
return 1;
}
/* 这个字占几格:ASCII 与 2 字节的拉丁扩展各 1 格,CJK/emoji(3、4 字节)占 2 格 —— 全角。
* How many cells this character takes: ASCII and the 2-byte Latin blocks take 1, CJK/emoji (3- and
* 4-byte) take 2 -- they are full width. */
static int cell_width(int lead) {
if (lead < 0x80) return 1;
if ((lead & 0xE0) == 0xC0) return 1;
return 2;
}
/* 从 `*pr` 行的 `*pc` 格放一个占 `w` 格的字:放不下就**软换行**到下一行行首。
* Put one character that takes `w` cells at row `*pr`, column `*pc`: if it does not fit, **soft-wrap**
* to the start of the next row.
* ⚠ **画正文与定位光标必须共用这一处** —— 各写一套,光标就会"停在行尾而字掉到下一行"(见文件头 ③)。
* ⚠ **Drawing and the caret must share this one place** -- two copies and the caret "sits at the end of
* the line while the character lands at the start of the next" (pitfall 3 in the file header). */
static void cell_fit(int *pr, int *pc, int w) {
if (*pc + w > g_cols) { *pr = *pr + 1; *pc = 0; }
}
/* 光标所在的**显示行/列**(走一遍正文得到;`\n` 与软换行都算)
* The **display row/column** of the caret (found by walking the text; `\n` and soft wraps both count). */
static void cursor_rc(int idx, int *orow, int *ocol) {
int r = 0;
int c = 0;
int i = 0;
int ch;
int n;
while (i < idx) {
ch = nbuf[i] & 0xFF;
if (ch == 10) { r = r + 1; c = 0; i = i + 1; continue; }
n = utf8_len(ch);
if (i + n > blen) n = 1; /* 缓冲尾上只剩半个序列(不该发生)*/
/* half a sequence at the tail */
cell_fit(&r, &c, cell_width(ch));
c = c + cell_width(ch);
i = i + n;
}
/* 正好落在一行末尾那一格之外 ⇒ 按下一行行首算 —— 这与"再打一个字会被放到哪儿"是同一个判据 */
/* Exactly past the last cell of a row means the next row's start -- the same judge as "where would
* the next character go". */
cell_fit(&r, &c, 1);
*orow = r;
*ocol = c;
}
/* 行数与字数(只给标题栏看)*/
/* Line and character counts (for the title bar only) */
static int count_lines(void) {
int i;
int n = 1;
for (i = 0; i < blen; i++) if ((nbuf[i] & 0xFF) == 10) n = n + 1;
return n;
}
static int count_chars(void) {
int i = 0;
int n = 0;
int ch;
while (i < blen) {
ch = nbuf[i] & 0xFF;
i = i + utf8_len(ch);
if (ch != 10) n = n + 1; /* 换行不算"字" */
/* a line break is not a "character" */
}
return n;
}
static void layout(void) {
int usable;
int body;
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 480; /* 宿主答不上来时的兜底(开窗也会用同一对数)*/
if (SH <= 0) SH = 640; /* fallback when the host cannot answer (the same pair opens the window) */
g_pad = SW / 20;
if (g_pad < 8) g_pad = 8;
g_fs = SW / 30; /* 字号随宽度走:手机窄屏小一点,桌面大一点 */
if (g_fs < 11) g_fs = 11; /* The font follows the width: smaller on a narrow phone, larger on the desktop. */
if (g_fs > 17) g_fs = 17;
g_cw = g_fs + 2; /* 一格宽 = 字高 + 2px 字距 */
/* one cell = the font height plus 2px of tracking */
g_lh = g_fs + 7; /* 行高(比字高大一半行距,多行看着才不挤)*/
/* line height */
g_y0 = g_pad + 34; /* 标题栏占掉上面一条 */
/* the title bar takes the strip above */
usable = SW - g_pad * 2;
g_cols = usable / g_cw;
if (g_cols < 8) g_cols = 8;
if (g_cols > 40) g_cols = 40; /* 宽窗口下别把一行拉得太长 */
/* do not stretch one line forever on a wide window */
g_x0 = (SW - g_cols * g_cw) / 2; /* 整块水平居中 */
/* centre the block horizontally */
if (g_x0 < g_pad) g_x0 = g_pad;
body = SH - g_pad - 46 - g_y0; /* 底下给两行提示留位置 */
/* two hint lines get the bottom strip */
if (body < g_lh * 3) body = g_lh * 3;
g_rows = body / g_lh;
if (g_rows < 1) g_rows = 1;
}
static void draw(void) {
int crow;
int ccol;
int r;
int c;
int i;
int n;
int w;
int ch;
int y;
int k;
int bar;
int ink;
int ptop;
int pbot;
char u[8];
char tb[64];
/* ① 先算光标在哪一格 —— 滚动要照着它来,所以**必须在画之前**算好 */
/* ① Work out the caret cell **before drawing**: scrolling follows it. */
cursor_rc(tcur, &crow, &ccol);
g_top = 0;
if (crow >= g_rows) g_top = crow - g_rows + 1;
ui_2d_clear(C_BG);
/* ② 标题栏:左边文件名,右边行数 / 字数 */
/* ② Title bar: the file name on the left, line and character counts on the right. */
ui_2d_text_styled(g_pad, g_pad, NOTE_FILE, C_TITLE, g_fs + 3, VML_ANCHOR_LEFT, VML_FONT_BOLD);
tb[0] = 0;
strcat(tb, numstr(count_lines()));
strcat(tb, lang == 0 ? " 行 · " : " lines · ");
strcat(tb, numstr(count_chars()));
strcat(tb, lang == 0 ? " 字" : " chars");
ui_2d_text(SW - g_pad, g_pad + 4, tb, C_DIM, g_fs - 2, VML_ANCHOR_RIGHT);
/* ③ 正文:一块"纸"+ 逐字上格
* ③ The body: a sheet of "paper" plus one character per cell. */
/* ⚠ `ui_2d_text` 给的 y **不是"字盒的顶"而是贴着基线**(实测:字号 16、y=58 时,
* ⚠ The y given to `ui_2d_text` is **not the top of the glyph box but the baseline** (measured:
* 拉丁字母的墨迹在 48..57、汉字在 47..59 —— 都在 y 的**上方**)。
* at size 16 with y=58 the Latin ink sits at 48..57 and CJK ink at 47..59, both **above** y).
* 所以纸面要按"墨迹高出多少"往上让,光标也要跟着往上抬 —— 不然光标会画在自己那一行的
* So the sheet and the caret both have to account for that overhang; otherwise the caret is drawn
* 自己那一行的下面(看着像"光标跳到了下一行",实测过)。
* below its own line (it reads as if the caret had jumped a line -- measured). */
ink = (g_fs * 3) / 4; /* 墨迹高出 y 的量(字号 16 ⇒ 12)*/
/* the overhang (size 16 => 12) */
ptop = g_y0 - ink - 5;
pbot = g_y0 + (g_rows - 1) * g_lh + g_fs + 6;
ui_2d_rect(g_x0 - 6, ptop, g_cols * g_cw + 12, pbot - ptop, C_PAPER, 1, 0, 8);
r = 0;
c = 0;
i = 0;
while (i < blen) {
ch = nbuf[i] & 0xFF;
if (ch == 10) { r = r + 1; c = 0; i = i + 1; continue; }
/* ⚠ **按码点切,不按字节切**:一个汉字是 3 个字节,逐字节画出来就是 3 个乱码格
* ⚠ **Cut by code point, never by byte**: a CJK character is 3 bytes, so drawing byte by byte
* (实测:`第一行中文` 画成了 `τ □ □ Σ …` 这样一排盒子)。
* yields three mojibake cells (measured: a row of `τ □ □ Σ` boxes came out of it). */
n = utf8_len(ch);
if (i + n > blen) n = 1; /* 末尾只剩半个序列(不该发生)*/
/* half a sequence at the tail */
w = cell_width(ch);
cell_fit(&r, &c, w);
if (r >= g_top && r < g_top + g_rows) {
y = g_y0 + (r - g_top) * g_lh;
k = 0;
while (k < n) { u[k] = nbuf[i + k]; k = k + 1; }
u[n] = 0;
ui_2d_text(g_x0 + c * g_cw, y, u, C_TEXT, g_fs, VML_ANCHOR_LEFT);
}
c = c + w;
i = i + n;
}
/* ④ 空文档给一行欢迎语(**只是占位,不进 `nbuf`** —— 否则第一次退出就会把欢迎语
* ④ An empty document shows a welcome line (**a placeholder only, never put into `nbuf`** --
* 当成用户的内容存进 `note.txt`)。 */
/* otherwise the first exit would save the welcome line into `note.txt` as the user's text.) */
if (blen == 0) {
ui_2d_text(g_x0, g_y0, lang == 0 ? "空白笔记 —— 直接打字" : "empty note - just type",
C_DIM, g_fs, VML_ANCHOR_LEFT);
ui_2d_text(g_x0, g_y0 + g_lh, lang == 0 ? "Esc 保存并退出" : "Esc saves and quits",
C_DIM, g_fs, VML_ANCHOR_LEFT);
}
/* ⑤ 光标:一条竖条,画在**当前字符位置**上(正好在一行末尾时它会落在下一行行首)
* ⑤ The caret: a vertical bar drawn **at the current character position** (at the end of a full row
* it lands at the start of the next one). */
if (crow >= g_top && crow < g_top + g_rows) {
bar = g_fs / 6;
if (bar < 2) bar = 2;
if (bar > 4) bar = 4;
y = g_y0 + (crow - g_top) * g_lh;
ui_2d_rect(g_x0 + ccol * g_cw, y - ink + 1, bar, g_fs + 3, C_CUR, 1, 0, 0);
}
/* ⑥ 底部:操作提示 + 文件状态(第二次启动一眼能看出"上次存的内容读回来了")*/
/* ⑥ Bottom: the controls and the file state (a second run shows at a glance that the last session
* was read back). */
ui_2d_text(g_pad, SH - g_pad - 32, lang == 0 ? "回车 换行 · 退格 删 · Esc 保存并退出"
: "Enter newline · Backspace delete · Esc save & quit",
C_HINT, g_fs - 3, VML_ANCHOR_LEFT);
tb[0] = 0;
strcat(tb, lang == 0 ? "文件 " : "file ");
strcat(tb, NOTE_FILE);
if (tloaded != 0) {
strcat(tb, lang == 0 ? " 已读取 " : " loaded ");
strcat(tb, numstr(blen));
strcat(tb, lang == 0 ? " 字节" : " bytes");
} else {
strcat(tb, lang == 0 ? " 新建(退出时写入)" : " new (written on exit)");
}
ui_2d_text(g_pad, SH - g_pad - 14, tb, C_HINT, g_fs - 3, VML_ANCHOR_LEFT);
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* ⚠ without this call you get a black screen */
}
/* ── 文件(⚠ 读用 0、写用 1,见文件头 ①)─────────────────────────── */
/* -- Files (⚠ read = 0, write = 1, see pitfall 1 in the file header) -- */
/* `\r` 一律清掉:本程序自己写的是 LF,可**别人**(Windows 记事本、Git 的 `core.autocrlf`)留下的
* Drop every `\r`: this program writes LF, but files left by **someone else** (Windows Notepad, or Git's
* 文件是 CRLF。`\r` **看不见,但在这个"一个字一格"的模型里照样占掉一格** —— 数数与光标都会把它算进去
* `core.autocrlf`) are CRLF. A `\r` **is invisible yet still takes a cell in this "one character per
* (实测:CRLF 的文件被算成 "18 字"、原样存回 20 字节;`ab\r\ncd` 上光标停在 `ab` 右边一格空位)。
* cell" model** -- both the count and the caret count it (measured: a CRLF file reads as "18 characters"
* 单独的 CR(老 Mac 的换行)也算一次换行 —— 不然整份文件会挤成一行。写回去时统一成 LF。
* and saves back as 20 bytes; on `ab\r\ncd` the caret sits one empty cell right of `ab`). A lone CR (a
* classic-Mac line break) counts as a line break too, otherwise the whole file collapses onto one line.
* Writes go back out as LF only. */
static void strip_cr(void) {
int i = 0;
int o = 0;
int ch;
while (i < blen) {
ch = nbuf[i] & 0xFF;
if (ch == 13) {
if (i + 1 < blen && (nbuf[i + 1] & 0xFF) == 10) { i = i + 1; continue; } /* CRLF ⇒ 只留 LF */
/* CRLF -> keep the LF */
nbuf[o] = 10; /* 光秃秃的 CR ⇒ 当成换行 */
/* a bare CR becomes a line break */
o = o + 1;
i = i + 1;
continue;
}
nbuf[o] = nbuf[i];
o = o + 1;
i = i + 1;
}
blen = o;
}
/* 读回来。返回 1 表示读到了(`blen`/`tcur` 已就位),0 表示"没有这个文件"(保持空文档)。
* Reads it back. Returns 1 when it was read (`blen`/`tcur` are set), 0 for "no such file" (the document
* stays empty). */
static int load_file(void) {
int mode;
int fh;
int n;
mode = 0; /* 0 = 只读(FileMode.Open)—— 写成 1 会先把文件清空 */
/* 0 = read-only (FileMode.Open); a 1 here would truncate the file first */
fh = fopen(NOTE_FILE, mode);
if (fh < 0) return 0; /* 错误码是**负数**(-200 文件不存在 / -202 越界)*/
/* error codes are **negative** */
n = fread(fh, nbuf, NOTE_CAP - 1);
fclose(fh);
if (n < 0) n = 0;
blen = n;
strip_cr(); /* ⚠ 见 `strip_cr`:`\r` 看不见却占一格,光标会浮在文字右边 */
/* ⚠ See `strip_cr`: a `\r` is invisible yet takes a cell, so the caret
floats right of the text. */
nbuf[blen] = 0; /* 存盘时不用它,但调试/打印时省事 */
/* not needed for saving, handy for printing */
tcur = blen; /* 光标停在上次内容的末尾,接着往下打就是追加 */
/* The caret sits at the end, so typing continues where the user left off. */
return 1;
}
/* 写回去。返回写成功的字节数(失败返回 -1)*/
/* Writes it back. Returns the bytes written, or -1. */
static int save_file(void) {
int mode;
int fh;
int w;
mode = 1; /* 1 = 只写(FileMode.Create,**先截断**)—— 所以我们才要整份重写 */
/* 1 = write-only (FileMode.Create, **truncating**) -- hence a full rewrite */
fh = fopen(NOTE_FILE, mode);
if (fh < 0) return -1;
w = fwrite(fh, nbuf, blen);
fclose(fh);
if (w != blen) return -1;
return w;
}
/* ── 编辑 ───────────────────────────────────────────────────────────── */
/* -- Editing -- */
/* 在光标处插一个字节(可打印 ASCII 或 `\n`)。缓冲满了就**丢掉**(记事本不该因为打满而崩)。
* Inserts one byte at the caret (a printable ASCII character or `\n`). A full buffer **drops** the key
* (a notepad must not fall over when it fills up). */
static void insert_ch(int ch) {
int i;
if (blen >= NOTE_CAP - 1) return;
i = blen;
while (i > tcur) { nbuf[i] = nbuf[i - 1]; i = i - 1; }
nbuf[tcur] = (char)ch;
blen = blen + 1;
tcur = tcur + 1;
}
/* 退格:删掉光标**前一个字符**。⚠ 要跨过**整个 UTF-8 序列** —— 只退一个字节会把一个汉字
* Backspace: delete the character **before** the caret. ⚠ It has to step over the **whole UTF-8
* 劈成"前导字节 + 半个身子",屏幕上是乱码、存盘后文件里也是坏字节(实测)。
* sequence** -- dropping one byte splits a CJK character into "lead byte + half a body", which is
* mojibake on screen and broken bytes in the file (measured). */
static void backspace(void) {
int start;
int n;
int i;
if (tcur <= 0) return;
start = tcur - 1;
while (start > 0 && ((nbuf[start] & 0xFF) & 0xC0) == 0x80) start = start - 1;
n = tcur - start;
i = start;
while (i + n < blen) { nbuf[i] = nbuf[i + n]; i = i + 1; }
blen = blen - n;
tcur = start;
}
/* 光标左移一个**字符**(不是一字节,同理)*/
/* Move the caret one **character** left (not one byte, same reason). */
static void cursor_left(void) {
int i;
if (tcur <= 0) return;
i = tcur - 1;
while (i > 0 && ((nbuf[i] & 0xFF) & 0xC0) == 0x80) i = i - 1;
tcur = i;
}
/* 光标右移一个字符 */
/* Move the caret one character right. */
static void cursor_right(void) {
int n;
if (tcur >= blen) return;
n = utf8_len(nbuf[tcur] & 0xFF);
tcur = tcur + n;
if (tcur > blen) tcur = blen;
}
/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
int msg[4];
int t;
int t0;
int done;
int k;
int wrote;
lang = ui_scr_get_language();
/* 开窗:**传 `0, 0`(= 尺寸交给宿主),开窗之后**再取一次尺寸 —— 那次才是**真实画布**
* Open the window: **pass `0, 0` (let the host size it) and take the size again after the open** -- that
* (开窗前的估算**可能不同**:桌面 = 480×640、手机竖屏 ~395×744),窗口与画布才是一套坐标系。
* is the **real canvas** (the pre-open estimate may differ: desktop 480x640, phone portrait about 395x744),
* so window and canvas share one coordinate space. */
layout();
ui_win_open_ex("notepad.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout();
tloaded = load_file();
if (tloaded == 0) { blen = 0; tcur = 0; } /* 没有文件 = 空文档 + 一行欢迎语(见 `draw` ④)*/
/* no file = an empty document plus a welcome line (see `draw` 4) */
print_str("[notepad] ");
print_str(NOTE_FILE);
if (tloaded != 0) {
print_str(lang == 0 ? " 读到 " : " loaded ");
print_int(blen);
print_str(lang == 0 ? " 字节\n" : " bytes\n");
} else {
print_str(lang == 0 ? " 还没有,从空文档开始(内容只能是这个程序的输入)\n"
: " not found, starting empty (content is only what you type here)\n");
}
draw();
t0 = ui_time_get_ms();
done = 0;
while (!done && ui_win_is_closed() == 0) {
t = ui_msg_wait(msg, 200);
if (t == VML_MSG_NONE) {
if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1; /* 空闲超时(验收用)*/
/* idle timeout */
continue;
}
t0 = ui_time_get_ms();
if (t == VML_MSG_KEYDOWN) {
k = msg[1];
/* ⚠ **顺序是有讲究的:方向键那一支必须在"可打印 ASCII"之前** ——
* ⚠ **The order matters: the arrow-key arms must come before the "printable ASCII" one** --
* `VML_KEY_LEFT/UP/RIGHT/DOWN` = 37/38/39/40,**全都落在 32..126 里**,
* `VML_KEY_LEFT/UP/RIGHT/DOWN` are 37/38/39/40, **all inside 32..126**, so testing the
* 先判可打印的话,按一下左箭头会往正文里**插一个 `%`**(实测:连按三下左箭头,
* printable range first turns one Left press into **a literal `%` in the text** (measured:
* 存下来的文件里多了 3 个 `%`,光标一步没动)。
* three Left presses left three `%` in the saved file and moved the caret not at all). */
if (k == VML_KEY_ESCAPE) { /* Esc = 保存并退出(保存统一在循环外做)*/
done = 1; /* Esc = save and quit (the save happens after the loop) */
} else if (k == VML_KEY_BACKSPACE) {
backspace();
draw();
} else if (k == VML_KEY_ENTER) {
insert_ch(10); /* ⚠ 回车要变成缓冲里的 10,见文件头 ④ */
draw(); /* ⚠ Enter becomes a 10 in the buffer, see pitfall 4 */
} else if (k == VML_KEY_LEFT) {
cursor_left();
draw();
} else if (k == VML_KEY_RIGHT) {
cursor_right();
draw();
} else if (k >= 32 && k <= 126) {
insert_ch(k);
draw();
}
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
layout();
draw();
continue;
}
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
/* ⚠ **收尾只有这一处**:`Esc`、空闲 10 秒、用户点了窗口的返回箭头,三条路都从这里出去。
* ⚠ **This is the only exit**: `Esc`, ten idle seconds and the window's back arrow all leave here.
* 把保存写进 `Esc` 那一支 = 另外两条路悄悄丢内容(文件头 ⑤)。
* Writing the save inside the `Esc` arm would silently lose the text on the other two (header 5). */
wrote = save_file();
print_str(lang == 0 ? "[notepad] 写入 " : "[notepad] wrote ");
print_str(NOTE_FILE);
print_str(" ");
print_int(wrote < 0 ? 0 : wrote);
print_str(lang == 0 ? " 字节,退出\n" : " bytes, quitting\n");
return 0;
}