basic/app/notepad.bas · 38772 字节 · 下载原文
' notepad.bas —— 常用小程序④:**记事本** —— 由 `Examples/c/app/notepad.c` 移植
' notepad.bas -- Everyday utility #4: a **notepad**, ported from `Examples/c/app/notepad.c`
'
' 一扇窗口里的纯文本编辑器:**打字 / 退格 / 回车换行 / 光标可见**,`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 program 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; Left/Right move the caret.
' · `Esc`(27) = **保存并退出**;空闲 10 秒也退出,且**走同一条收尾**(见下面第 ⑤ 条)。
' · `Esc` (27) = **save and quit**; 10 s of idling 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 (absolute paths are
' rejected); on the desktop it lands in the **directory of the source file**.
' 跑法(桌面脚手架,秒级)/ How to run (desktop scaffolding, seconds):
' dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll third_party/vml/Examples/basic/app/notepad.bas --vml-home third_party/vml
' 手机上:命令行页里敲 `vml run examples/basic/app/notepad.bas`。
' On the phone: type `vml run examples/basic/app/notepad.bas` in the command-line page.
'
' ## 踩过的坑(实测)
' ## 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"读到的是**空串**。读=0、写=1,两个常量**各写一次、别共用**。
' **truncates first**, so "meant to read, passed 1" hands back an **empty string**. Read = 0, write = 1, never shared.
'
' ② **正文不能"一个字节画一格"**:UTF-8 里一个汉字是 3 个字节。实测逐字节画出来是一排 `τ □ □ Σ` 盒子;
' ② **The body must not be drawn "one byte per cell"**: a CJK character is 3 bytes in UTF-8. Drawn byte by byte
' **按码点切**才对。
' it came out as a row of boxes; cutting **by code point** is what works.
'
' ③ **"一格放得下吗"只有一处判据**:画正文与定位光标若各写一套换行规则,就会出现"光标停在行尾、
' ③ **"Does it fit in a cell" has exactly one judge**: two copies of the wrap rule -- one for drawing, one for
' 字却掉到下一行行首"。共用 `cell_fit`(这里用两个全局量 `cf_r`/`cf_c` 代替 C 的指针形参)。
' the caret -- produce "the caret sits at the end of the line while the character lands on the next". Both go
' through `cell_fit` (two globals `cf_r`/`cf_c` stand in for C's pointer parameters).
'
' ④ **回车就是缓冲里的字节 `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.
'
' ⑤ **收尾只有一处**:`Esc` / 空闲 10 秒 / 关窗三条路都从循环**之后**那一段出去。把保存写进 `Esc` 那一支
' ⑤ **A single exit**: `Esc`, ten idle seconds and the window's close arrow all leave through the one place
' = 另外两条路**悄悄丢内容**。
' **after** the loop. Putting the save inside the `Esc` arm would silently lose the text on the other two.
'
' ⑥ **`ui_2d_text` 的 `y` 是基线,不是"字盒的顶"**:墨迹落在 `y` 的**上方**。实测(字号 16、`y=58`):
' ⑥ **`ui_2d_text`'s `y` is the baseline, not the top of the glyph box**: the ink sits **above** it. Measured
' 拉丁字母的墨迹在 48..57、汉字在 47..59。按"y 是盒顶"去画光标,光标就落在**自己那一行的下面**。
' (size 16, y=58): Latin ink at 48..57, CJK ink at 47..59. Draw the caret assuming "y is the top" and it lands
' **below its own line**. See `ink` in `render`.
'
' ⑦ **读进来的 `\r` 要清掉**:本程序写的是 LF,但**别人的**文件(Windows 记事本、Git 的 `core.autocrlf`)
' ⑦ **Strip the `\r` on the way in**: this program writes LF, but files from **someone else** (Windows Notepad,
' 是 CRLF。`\r` **看不见,却按"一个字"占掉一格**(`ab\r\ncd` 上光标会停在 `ab` 右边一格空位)。
' or Git's `core.autocrlf`) are CRLF. A `\r` **is invisible yet takes one cell** (on `ab\r\ncd` the caret sits
' one empty cell right of `ab`). Write back as LF only.
'
' ⑧ **`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"**, so testing the
' 所以**先判可打印**就会把方向键当成字符插进去(实测连按三下左箭头,正文里多了 3 个 `%`)。
' printable range first types the arrow key in as a character (measured: three Left presses left three `%`).
' 判 `VML_MSG_KEYDOWN` 时**特殊键一律排在可打印区间之前**。
' When handling `VML_MSG_KEYDOWN`, **special keys always come before the printable range**.
'
' ── BASIC 这一门额外的一条(实测,决定了正文模型怎么写)────────────
' -- One extra BASIC-only fact (measured; it decides how the text model is written) --
' **字符串临时量在 256 字节处封顶**(库里的轮转池 `_strbuf[8][256]` 与 `_concats[16][256]`),
' **String temporaries cap out at 256 bytes** (the library's `_strbuf[8][256]` and `_concats[16][256]` pools),
' 而 C 版的正文档位是 2048 字节 ⇒ **正文存成一个整型数组,一个字节一格**(`doc(i)`),
' while the C version's document is 2048 bytes => **the document is an integer array, one byte per cell** (`doc(i)`).
' 一切编辑都是数组搬移,不碰字符串上限。只有三处需要变成字符串:画一个字(≤4 字节)、
' All editing is array shuffling and never touches the string cap. Only three places need strings: drawing one
' 读文件时从**裸内存**里按字节取、存盘时**每攒够 200 字节写一次**(分块写同一个句柄,落盘的是同一串字节)。
' character (<=4 bytes), reading file bytes out of **raw memory**, and saving in **200-byte chunks** (several
' writes to the same handle put the same bytes on disk).
' ⚠ 这条不是洁癖:写成 `nbuf = LEFT$(nbuf, n) + s$ + MID$(nbuf, n+1)` 的话,**文档在 255 字节处被静默截断**。
' ⚠ This is not fussiness: with `nbuf = LEFT$(nbuf, n) + s$ + MID$(nbuf, n+1)` the **document is silently truncated at 255 bytes**.
' ── UI 接口:引用**库侧**的声明文件 ────────────────────────────────
' ── UI interface: include the **library-side** declaration file ────────────────────────────────
'$INCLUDE: 'vml_ui.bas'
' ── 共享库里的文件 / 堆(都在 `Lib/shared/vmlsys.vml`,随 builtins 自动链上)──
' ── Files and heap from the shared library (all in `Lib/shared/vmlsys.vml`, auto-linked with builtins) ──
' ⚠ `buf` 写 `AS INTEGER` 就是"这个整数就是地址"(收数据用);写 `AS STRING` 是"把这个串的地址传过去"(发数据用)。
' ⚠ `buf AS INTEGER` means "the integer is the address" (for receiving); `buf AS STRING` means "pass this string's address".
NATIVE FUNCTION vml_file_open(path AS STRING, mode AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_file_read(handle AS INTEGER, addr AS INTEGER, ln AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_file_write(handle AS INTEGER, buf AS STRING, ln AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_file_close(handle AS INTEGER) AS INTEGER
' `strcpy` 只借它"返回 dst"这一条:自拷一次就把一块字面量的地址拿到手(见文件头那条 BASIC 说明)。
' `strcpy` is borrowed purely for "it returns dst": one self-copy yields a literal's address (see the BASIC note above).
NATIVE FUNCTION strcpy(dst AS STRING, src AS STRING) AS INTEGER
' ══ 常量 ══════════════════════════════════════════════════════
' ══ Constants ══════════════════════════════════════════════════
DIM IDLE_EXIT_MS AS INTEGER
DIM NOTE_CAP AS INTEGER
DIM NOTE_FILE AS STRING
' 配色(真彩 AARRGGBB,第 4 层没有索引色)
' Colours (true colour AARRGGBB; layer 4 has no palette)
DIM C_BG AS INTEGER
DIM C_PAPER AS INTEGER
DIM C_TITLE AS INTEGER
DIM C_TEXT AS INTEGER
DIM C_DIM AS INTEGER
DIM C_CUR AS INTEGER
DIM C_HINT AS INTEGER
' ══ 正文模型:**一维扁平缓冲**(一个字节一格)──────────────────────
' ══ The text model: **one flat buffer**, one byte per cell ──────────────────────
' ⚠ 不用二维数组、不用"指针数组"、不取数组元素的地址 —— 这几样在本仓的多语言前端之间最不稳。
' ⚠ No 2-D arrays, no arrays of pointers, no address-of on an element: the least portable forms across this repo's front ends.
DIM doc(2048) AS INTEGER
DIM blen AS INTEGER
DIM tcur AS INTEGER
DIM tloaded AS INTEGER
' ══ 全局状态 ══════════════════════════════════════════════════
' ══ Global state ══════════════════════════════════════════════════
DIM SW AS INTEGER
DIM SH AS INTEGER
DIM lang AS INTEGER
DIM g_pad AS INTEGER
DIM g_fs AS INTEGER
DIM g_cw AS INTEGER
DIM g_lh AS INTEGER
DIM g_x0 AS INTEGER
DIM g_y0 AS INTEGER
DIM g_cols AS INTEGER
DIM g_rows AS INTEGER
DIM g_top AS INTEGER
DIM base AS INTEGER
DIM rbuf AS STRING
' 主循环用的几个(**必须先 DIM 再赋值**)。
' A few used by the main loop (**DIM before assignment**).
DIM t0 AS INTEGER
DIM done AS INTEGER
DIM msg_t AS INTEGER
DIM k AS INTEGER
DIM wrote AS INTEGER
' `cell_fit` 的两个"出参"(C 版是 `int*` 形参,BASIC 没有指针 ⇒ 用全局量)。
' The two "out parameters" of `cell_fit` (C passes `int*`; BASIC has no pointers, so two globals do the job).
DIM cf_r AS INTEGER
DIM cf_c AS INTEGER
' 文案(开局按宿主语言算一次)
' Strings (computed once at startup from the host language)
DIM sWin AS STRING
DIM sWelcome AS STRING
DIM sQuitHint AS STRING
DIM sEditHint AS STRING
DIM sFileLoaded AS STRING
DIM sFileNew AS STRING
DIM sLines AS STRING
DIM sChars AS STRING
DIM sFileWord AS STRING
DIM sLoadedWord AS STRING
DIM sBytesWord AS STRING
SUB initLang()
lang = ui_scr_get_language()
IF lang = 0 THEN
sWin = "记事本(BASIC)"
sWelcome = "空白笔记 —— 直接打字"
sQuitHint = "Esc 保存并退出"
sEditHint = "回车 换行 · 退格 删 · Esc 保存并退出"
sFileLoaded = " 已读取 "
sFileNew = " 新建(退出时写入)"
sLines = " 行 · "
sChars = " 字"
sFileWord = "文件 "
sLoadedWord = " 读到 "
sBytesWord = " 字节"
ELSE
sWin = "Notepad (BASIC)"
sWelcome = "empty note - just type"
sQuitHint = "Esc saves and quits"
sEditHint = "Enter newline · Backspace delete · Esc save & quit"
sFileLoaded = " loaded "
sFileNew = " new (written on exit)"
sLines = " lines · "
sChars = " chars"
sFileWord = "file "
sLoadedWord = " loaded "
sBytesWord = " bytes"
END IF
END SUB
' ── 小工具 ───────────────────────────────────────────────────
' ── Small helpers ───────────────────────────────────────────────────
' 这个字节起头是几个字节的 UTF-8 序列(认不出按 1 算)。
' How many bytes the UTF-8 sequence starting here is (unrecognized counts as 1).
FUNCTION utf8_len(lead AS INTEGER) AS INTEGER
utf8_len = 1
IF lead >= 128 THEN
IF (lead AND 224) = 192 THEN
utf8_len = 2
END IF
IF (lead AND 240) = 224 THEN
utf8_len = 3
END IF
IF (lead AND 248) = 240 THEN
utf8_len = 4
END IF
END IF
END FUNCTION
' 这个字占几格: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.
FUNCTION cell_width(lead AS INTEGER) AS INTEGER
cell_width = 2
IF lead < 128 THEN
cell_width = 1
END IF
IF lead >= 192 AND lead < 224 THEN
cell_width = 1
END IF
END FUNCTION
' 从 `cf_r` 行的 `cf_c` 格放一个占 `w` 格的字:放不下就**软换行**到下一行行首。
' Put one character that takes `w` cells at row `cf_r`, column `cf_c`: if it does not fit, **soft-wrap** to 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 on the start of the next" (pitfall 3).
SUB cell_fit(w AS INTEGER)
IF cf_c + w > g_cols THEN
cf_r = cf_r + 1
cf_c = 0
END IF
END SUB
' 整数 → 十进制串(不补零)。借库里的 `STR$` 再 `LTRIM$`。
' Integer to a decimal string (no padding). Borrow `STR$` plus `LTRIM$`.
FUNCTION num2s(v AS INTEGER) AS STRING
num2s = LTRIM$(STR$(v))
END FUNCTION
' 行数与字数(只给标题栏看)
' Line and character counts (for the title bar only)
FUNCTION count_lines() AS INTEGER
DIM i AS INTEGER
DIM n AS INTEGER
n = 1
i = 0
WHILE i < blen
IF doc(i) = 10 THEN
n = n + 1
END IF
i = i + 1
WEND
count_lines = n
END FUNCTION
FUNCTION count_chars() AS INTEGER
DIM i AS INTEGER
DIM n AS INTEGER
DIM ch AS INTEGER
i = 0
n = 0
WHILE i < blen
ch = doc(i)
i = i + utf8_len(ch)
' 换行不算"字"
' A line break is not a "character"
IF ch <> 10 THEN
n = n + 1
END IF
WEND
count_chars = n
END FUNCTION
' ── 编辑 ─────────────────────────────────────────────────────────────
' ── Editing ─────────────────────────────────────────────────────────────
' 在光标处插一个字节(可打印 ASCII 或 `\n`)。缓冲满了就**丢掉**(记事本不该因为打满而崩)。
' Inserts one byte at the caret (printable ASCII or `\n`). A full buffer **drops** the key.
SUB insert_ch(ch AS INTEGER)
DIM i AS INTEGER
IF blen < NOTE_CAP - 1 THEN
i = blen
WHILE i > tcur
doc(i) = doc(i - 1)
i = i - 1
WEND
doc(tcur) = ch
blen = blen + 1
tcur = tcur + 1
END IF
END SUB
' 退格:删掉光标**前一个字符**。⚠ 要跨过**整个 UTF-8 序列** —— 只退一个字节会把一个汉字劈成"前导字节 + 半个身子"。
' Backspace: delete the character **before** the caret. ⚠ Step over the **whole UTF-8 sequence**, or a CJK
' character is split into "lead byte + half a body" (mojibake on screen and broken bytes in the file).
SUB backspace()
DIM start AS INTEGER
DIM n AS INTEGER
DIM i AS INTEGER
DIM go AS INTEGER
IF tcur > 0 THEN
' 从光标前一格往回退,直到退到一个**首字节**(续字节的高两位都是 `10`)。
' Step back from the cell before the caret until a **lead byte** is reached (continuation bytes start with `10`).
start = tcur - 1
go = 1
WHILE start > 0 AND go = 1
IF (doc(start) AND 192) = 128 THEN
start = start - 1
ELSE
go = 0
END IF
WEND
n = tcur - start
i = start
WHILE i + n < blen
doc(i) = doc(i + n)
i = i + 1
WEND
blen = blen - n
tcur = start
END IF
END SUB
' 光标左移一个**字符**(不是一字节,同理)。
' Move the caret one **character** left (not one byte, same reason).
SUB cursor_left()
DIM i AS INTEGER
DIM go AS INTEGER
IF tcur > 0 THEN
i = tcur - 1
go = 1
WHILE i > 0 AND go = 1
IF (doc(i) AND 192) = 128 THEN
i = i - 1
ELSE
go = 0
END IF
WEND
tcur = i
END IF
END SUB
' 光标右移一个字符
' Move the caret one character right.
SUB cursor_right()
DIM n AS INTEGER
IF tcur < blen THEN
n = utf8_len(doc(tcur))
tcur = tcur + n
IF tcur > blen THEN
tcur = blen
END IF
END IF
END SUB
' 光标所在的**显示行/列**(走一遍正文得到;`\n` 与软换行都算)。
' The **display row/column** of the caret (found by walking the text; `\n` and soft wraps both count).
SUB cursor_rc(idx AS INTEGER)
DIM i AS INTEGER
DIM ch AS INTEGER
DIM n AS INTEGER
cf_r = 0
cf_c = 0
i = 0
WHILE i < idx
ch = doc(i)
IF ch = 10 THEN
cf_r = cf_r + 1
cf_c = 0
i = i + 1
ELSE
n = utf8_len(ch)
IF i + n > blen THEN
n = 1
' 缓冲尾上只剩半个序列(不该发生)
' half a sequence at the tail (should not happen)
END IF
cell_fit(cell_width(ch))
cf_c = cf_c + cell_width(ch)
i = i + n
END IF
WEND
' 正好落在一行末尾那一格之外 ⇒ 按下一行行首算 —— 这与"再打一个字会被放到哪儿"是同一个判据。
' 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(1)
END SUB
' ── 版面(**一律从宿主给的尺寸算**,别写死坐标)──────────────────
' ── Layout (**always computed from the host size**, never hard-coded coordinates) ──────────────────
SUB layout()
DIM usable AS INTEGER
DIM body AS INTEGER
SW = ui_scr_get_w()
SH = ui_scr_get_h()
IF SW <= 0 THEN
SW = 480
END IF
IF SH <= 0 THEN
SH = 640
END IF
g_pad = SW \ 20
IF g_pad < 8 THEN
g_pad = 8
END IF
' 字号随宽度走:手机窄屏小一点,桌面大一点。
' The font follows the width: smaller on a narrow phone, larger on the desktop.
g_fs = SW \ 30
IF g_fs < 11 THEN
g_fs = 11
END IF
IF g_fs > 17 THEN
g_fs = 17
END IF
g_cw = g_fs + 2
g_lh = g_fs + 7
g_y0 = g_pad + 34
usable = SW - g_pad * 2
g_cols = usable \ g_cw
IF g_cols < 8 THEN
g_cols = 8
END IF
IF g_cols > 40 THEN
g_cols = 40
END IF
g_x0 = (SW - g_cols * g_cw) \ 2
IF g_x0 < g_pad THEN
g_x0 = g_pad
END IF
body = SH - g_pad - 46 - g_y0
IF body < g_lh * 3 THEN
body = g_lh * 3
END IF
g_rows = body \ g_lh
IF g_rows < 1 THEN
g_rows = 1
END IF
END SUB
' ── 绘制(绘图全在 SUB 里;模块级只留调用)──────────────────────
' ── Drawing (all drawing lives in a SUB; module level only calls it) ──────────────────────
SUB render()
DIM crow AS INTEGER
DIM ccol AS INTEGER
DIM i AS INTEGER
DIM n AS INTEGER
DIM w AS INTEGER
DIM ch AS INTEGER
DIM y AS INTEGER
DIM k AS INTEGER
DIM bar AS INTEGER
DIM ink AS INTEGER
DIM ptop AS INTEGER
DIM pbot AS INTEGER
DIM u AS STRING
DIM tb AS STRING
DIM part AS STRING
' ① 先算光标在哪一格 —— 滚动要照着它来,所以**必须在画之前**算好。
' (1) Work out the caret cell **before drawing**: scrolling follows it.
' ⚠ 两个"出参"走全局量 `cf_r`/`cf_c`(BASIC 没有指针形参,按值传出去的形参改不回来)。
' ⚠ The two "out parameters" travel through the globals `cf_r`/`cf_c` (BASIC has no pointer parameters).
cursor_rc(tcur)
crow = cf_r
ccol = cf_c
g_top = 0
IF crow >= g_rows THEN
g_top = crow - g_rows + 1
END IF
ui_2d_clear(C_BG)
' ② 标题栏:左边文件名,右边行数 / 字数。
' (2) 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)
part = num2s(count_lines())
tb = part + sLines
part = num2s(count_chars())
tb = tb + part
tb = tb + sChars
ui_2d_text(SW - g_pad, g_pad + 4, tb, C_DIM, g_fs - 2, VML_ANCHOR_RIGHT)
' ③ 正文:一块"纸"+ 逐字上格
' (3) The body: a sheet of "paper" plus one character per cell
' ⚠ `ui_2d_text` 给的 y **不是"字盒的顶"而是贴着基线**(见文件头 ⑥),所以纸面与光标都要往上让。
' ⚠ The y given to `ui_2d_text` is **the baseline, not the top of the glyph box** (pitfall 6), so both the
' sheet and the caret have to account for that overhang.
ink = (g_fs * 3) \ 4
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)
' ⚠ 正文这一遍**直接用全局的 `cf_r`/`cf_c`**(不是另开一对局部 `r`/`c`)——
' ⚠ This pass uses the globals `cf_r`/`cf_c` **directly** (not a separate local pair `r`/`c`):
' C 版是把 `&r,&c` 传进 `cell_fit`,这里没有指针,所以"行/列"只能放全局量。
' C passes `&r,&c` into `cell_fit`; with no pointers the row/column has to live in globals.
' 实测(写成本地 `r`/`c` 那一版):`cell_fit` 改的是全局、循环读的是本地 ⇒ **软换行整个失效**,
' Measured (the local `r`/`c` version): `cell_fit` updated the globals while the loop read the locals, so
' 长行一路画出右边界(与 C 版逐像素不同)。
' **soft wrap was dead** and long lines ran off the right edge (pixel-different from the C version).
cf_r = 0
cf_c = 0
i = 0
WHILE i < blen
ch = doc(i)
IF ch = 10 THEN
cf_r = cf_r + 1
cf_c = 0
i = i + 1
ELSE
' ⚠ **按码点切,不按字节切**:一个汉字是 3 个字节,逐字节画出来就是 3 个乱码格(见文件头 ②)。
' ⚠ **Cut by code point, never by byte** (pitfall 2): a CJK glyph is 3 bytes and would become three mojibake cells.
n = utf8_len(ch)
IF i + n > blen THEN
n = 1
END IF
w = cell_width(ch)
cell_fit(w)
IF cf_r >= g_top AND cf_r < g_top + g_rows THEN
y = g_y0 + (cf_r - g_top) * g_lh
' 把这一个码点的**字节**拼成短串(≤4 字节,不碰 255 的边界)。
' Join the **bytes** of this one code point into a short string (<=4 bytes, well inside the 255 cap).
u = ""
k = 0
WHILE k < n
u = u + CHR$(doc(i + k))
k = k + 1
WEND
ui_2d_text(g_x0 + cf_c * g_cw, y, u, C_TEXT, g_fs, VML_ANCHOR_LEFT)
END IF
cf_c = cf_c + w
i = i + n
END IF
WEND
' ④ 空文档给一行欢迎语(**只是占位,不进 `doc`** —— 否则第一次退出就会把欢迎语当成用户的内容存进 `note.txt`)。
' (4) An empty document shows a welcome line (**a placeholder only, never put into `doc`** -- otherwise the first
' exit would save the welcome line into `note.txt` as the user's text).
IF blen = 0 THEN
ui_2d_text(g_x0, g_y0, sWelcome, C_DIM, g_fs, VML_ANCHOR_LEFT)
ui_2d_text(g_x0, g_y0 + g_lh, sQuitHint, C_DIM, g_fs, VML_ANCHOR_LEFT)
END IF
' ⑤ 光标:一条竖条,画在**当前字符位置**上(正好在一行末尾时它会落在下一行行首)。
' (5) 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 AND crow < g_top + g_rows THEN
bar = g_fs \ 6
IF bar < 2 THEN
bar = 2
END IF
IF bar > 4 THEN
bar = 4
END IF
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)
END IF
' ⑥ 底部:操作提示 + 文件状态(第二次启动一眼能看出"上次存的内容读回来了")。
' (6) 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, sEditHint, C_HINT, g_fs - 3, VML_ANCHOR_LEFT)
tb = sFileWord
tb = tb + NOTE_FILE
IF tloaded <> 0 THEN
tb = tb + sFileLoaded
part = num2s(blen)
tb = tb + part
tb = tb + sBytesWord
ELSE
tb = tb + sFileNew
END IF
ui_2d_text(g_pad, SH - g_pad - 14, tb, C_HINT, g_fs - 3, VML_ANCHOR_LEFT)
ui_2d_present()
END SUB
' ── 文件(⚠ 读用 0、写用 1,见文件头 ①)───────────────────────────
' ── Files (⚠ read = 0, write = 1, see pitfall 1) ───────────────────────────
' 读回来。返回 1 表示读到了,0 表示"没有这个文件"(保持空文档)。
' Reads it back. Returns 1 when it was read, 0 for "no such file" (the document stays empty).
' ⚠ **`\r` 一律清掉**:本程序写 LF,而**别人**留下的文件是 CRLF,而 `\r` 在这个"一个字一格"的模型里照样占一格。
' ⚠ **Every `\r` is dropped**: this program writes LF, files from others are CRLF, and a `\r` still takes a cell here.
FUNCTION load_file() AS INTEGER
DIM fh AS INTEGER
DIM n AS INTEGER
DIM i AS INTEGER
DIM b AS INTEGER
DIM nb AS INTEGER
DIM k AS INTEGER
load_file = 0
blen = 0
tcur = 0
fh = vml_file_open(NOTE_FILE, 0)
IF fh >= 0 THEN
n = vml_file_read(fh, base, NOTE_CAP - 1)
' ⚠ 关句柄的返回值**不能写回 `n`** —— 那会把刚读到的字节数冲成"关成功"的 0/1
' ⚠ The close result must **not** be written back into `n`: that would overwrite the byte count just read
' (实测症状:读 81 字节的文件报"读到 1684234849 字节"然后整屏乱画)。
' (measured symptom: an 81-byte file reported "read 1684234849 bytes" and the screen was scribbled).
k = vml_file_close(fh)
IF n < 0 THEN
n = 0
END IF
i = 0
WHILE i < n
b = ASC(MID$(rbuf, i + 1, 1))
IF b < 0 THEN
b = b + 256
END IF
IF b = 13 THEN
nb = 0
IF i + 1 < n THEN
nb = ASC(MID$(rbuf, i + 2, 1))
IF nb < 0 THEN
nb = nb + 256
END IF
END IF
IF nb = 10 THEN
' CRLF ⇒ 只留那个 LF(下一轮循环自己会收)
' CRLF -> keep the LF (the next iteration picks it up)
nb = 0
ELSE
' 光秃秃的 CR ⇒ 当成换行
' a bare CR becomes a line break
IF blen < NOTE_CAP - 1 THEN
doc(blen) = 10
blen = blen + 1
END IF
END IF
ELSE
IF blen < NOTE_CAP - 1 THEN
doc(blen) = b
blen = blen + 1
END IF
END IF
i = i + 1
WEND
tcur = blen
load_file = 1
END IF
END FUNCTION
' 写回去。返回写成功的字节数(失败返回 -1)。
' Writes it back. Returns the bytes written, or -1.
' ⚠ **分块写**:每攒够 200 字节写一次,同一个句柄的多次写就是同一串字节(见文件头那条 BASIC 特有的说明)。
' ⚠ **Written in chunks**: every 200 bytes go out in one call, and several writes to one handle are one byte stream.
FUNCTION save_file() AS INTEGER
DIM fh AS INTEGER
DIM i AS INTEGER
DIM w AS INTEGER
DIM total AS INTEGER
DIM chunk AS STRING
save_file = -1
fh = vml_file_open(NOTE_FILE, 1)
IF fh >= 0 THEN
total = 0
chunk = ""
i = 0
WHILE i < blen
chunk = chunk + CHR$(doc(i))
IF LEN(chunk) >= 200 THEN
w = vml_file_write(fh, chunk, LEN(chunk))
total = total + w
chunk = ""
END IF
i = i + 1
WEND
IF LEN(chunk) > 0 THEN
w = vml_file_write(fh, chunk, LEN(chunk))
total = total + w
END IF
w = vml_file_close(fh)
save_file = total
END IF
END FUNCTION
' ══ 主程序 ════════════════════════════════════════════════════
' ══ Main program ════════════════════════════════════════════════════
IDLE_EXIT_MS = 10000
NOTE_CAP = 2048
NOTE_FILE = "note.txt"
C_BG = &HFF101018
C_PAPER = &HFF1A1A24
C_TITLE = &HFFE8E8F0
C_TEXT = &HFFD8DCE8
C_DIM = &HFF9AA0B0
C_CUR = &HFFFFC64A
C_HINT = &HFF7A8090
' 读文件要一块 2112 字节的字面量缓冲(BASIC 的字符串临时量在 256 字节处封顶,装不下 2048 字节的笔记)。
' Reading needs a 2112-byte literal buffer (BASIC string temporaries cap at 256 bytes, too small for a 2048-byte note).
' 下面这一行**故意很长**:它必须是字面量,不能拆行、也不能用 `+` 拼(拼接在 256 字节处截断)。
' The line below is **deliberately long**: it must be a literal, not split or joined (concatenation truncates at 256).
' ⚠ **别用 `vml_alloc`** —— 那个堆会盖掉 BASIC 自己的全局变量(实测:读 81 字节后 `base` 变成文件里的 4 个字节)。
' ⚠ **`vml_alloc` is not used** -- that heap overwrites BASIC's own globals (measured: after an 81-byte read, `base`
' 变成文件里的 4 个字节)。地址改用 `strcpy` 自拷取得(它返回 `dst`)。
' became four bytes of the file). The address comes from a `strcpy` self-copy (it returns `dst`).
rbuf = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
base = strcpy(rbuf, rbuf)
initLang
' 开窗:**先问尺寸再开窗**(开窗后再问一次 —— 画布尺寸与开窗前的估算**可能不同**)。
' Open the window: **ask for the size first**, then ask again (the canvas may differ from the pre-open estimate).
layout()
ui_win_open_ex(sWin, 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)
layout()
tloaded = load_file()
IF tloaded = 0 THEN
blen = 0
tcur = 0
END IF
IF tloaded <> 0 THEN
PRINT "[notepad] "; NOTE_FILE; sLoadedWord; blen; sBytesWord
ELSE
PRINT "[notepad] "; NOTE_FILE; " 还没有,从空文档开始(内容只能是这个程序的输入)"
END IF
render()
t0 = ui_time_get_ms()
done = 0
WHILE done = 0
IF ui_win_is_closed() <> 0 THEN
done = 1
END IF
IF done = 0 THEN
msg_t = ui_msg_fetch_wait(200)
IF msg_t = VML_MSG_NONE THEN
' 空闲超时(自动验收用)
' Idle timeout (for automated checks)
IF ui_time_get_ms() - t0 > IDLE_EXIT_MS THEN
done = 1
END IF
END IF
IF msg_t <> VML_MSG_NONE THEN
t0 = ui_time_get_ms()
IF msg_t = VML_MSG_KEYDOWN THEN
' ⚠ **顺序是有讲究的:方向键那一支必须在"可打印 ASCII"之前**(见文件头 ⑧)。
' ⚠ **The order matters: the arrow arms must come before the "printable ASCII" one** (pitfall 8).
k = ui_msg_get_a()
IF k = VML_KEY_ESCAPE THEN
' Esc = 保存并退出(保存统一在循环外做)
' Esc = save and quit (the save happens after the loop)
done = 1
END IF
IF k = VML_KEY_BACKSPACE THEN
backspace()
render()
END IF
IF k = VML_KEY_ENTER THEN
' ⚠ 回车要变成缓冲里的 10(见文件头 ④)
' ⚠ Enter becomes a 10 in the buffer (pitfall 4)
insert_ch(10)
render()
END IF
IF k = VML_KEY_LEFT THEN
cursor_left()
render()
END IF
IF k = VML_KEY_RIGHT THEN
cursor_right()
render()
END IF
IF k >= 32 AND k <= 126 THEN
insert_ch(k)
render()
END IF
END IF
IF msg_t = VML_MSG_WINDOWORIENT OR msg_t = VML_MSG_WINDOWRESIZE THEN
layout()
render()
END IF
IF msg_t = VML_MSG_WINDOWCLOSE THEN
done = 1
END IF
END IF
END IF
WEND
' ⚠ **收尾只有这一处**:`Esc`、空闲 10 秒、用户点了窗口的返回箭头,三条路都从这里出去(见文件头 ⑤)。
' ⚠ **This is the only exit**: `Esc`, ten idle seconds and the window's back arrow all leave here (pitfall 5).
wrote = save_file()
PRINT "[notepad] 写入 "; NOTE_FILE; " "; wrote; " 字节,退出"