c/app/calendar.c · 15769 字节 · 下载原文
/* calendar.c —— 常用小程序①:**日历**(第 4 层:`ui_*` 图形界面)
* calendar.c -- Everyday utility #1: a **calendar** (layer 4: the `ui_*` graphical interface)
*
* 一扇窗口里的月历:**今天高亮**,`←`/`→` 翻月、`↑`/`↓` 翻年,**点某一天**也能选中它,
* A month calendar in a window: **today is highlighted**; Left/Right change the month, Up/Down the year,
* `Esc` 退出。手机上就是"点屏幕",桌面上是键盘或鼠标 —— 同一份源码两个平台都跑。
* and **tapping a day** selects it; Esc quits. On a phone that is a tap, on the desktop a key or a mouse --
* one source file, both platforms.
*
* ## 第 4 层的三条规矩(不守就会"手机上行、桌面上歪")
* ## Three rules of layer 4 (break them and it works on the phone but drifts on the desktop)
*
* · **尺寸从宿主问**:`ui_scr_get_w()/_h()`,别写死。窗口是按宿主给的尺寸开的,
* · **Ask the host for the size**: `ui_scr_get_w()/_h()`, never hard-code it. The window opens at the size
* 手机上竖屏 ~395 宽、桌面上是另一套数,写死就一边挤一边空。
* the host gives; a phone in portrait is about 395 wide while the desktop is something else entirely.
* · **颜色是真彩 `0xAARRGGBB`**(不是索引色):`0xFF101018` = 不透明的深灰。
* · **Colours are true colour `0xAARRGGBB`** (not palette indexes): `0xFF101018` is opaque near-black.
* · **画完必须 `ui_2d_present()`**:它是"把这一帧交上去"那一步,漏了就是黑屏(宿主不猜你画完了没)。
* · **Always finish with `ui_2d_present()`**: that is the "hand this frame over" step; skip it and you get a
* black screen (the host does not guess when you are done).
*
* ## 为什么"空闲 8 秒就自己退出"
* ## Why it exits by itself after 8 seconds of idling
*
* 例程要能被**自动验收**(`scripts/vmlcli` 加 `--frame` 出一张图就够了),所以循环必须有界:
* Samples have to be **automatically verifiable** (one `scripts/vmlcli` run with `--frame` is enough),
* so the loop must be bounded:
* 空闲超过 8 秒就退出。**写真正的程序时把 `IDLE_EXIT_MS` 去掉**,其余(画格子 / 读键 / 命中判定)就是完整写法。
* it exits after 8 s of idling. **Drop `IDLE_EXIT_MS` when you write a real program**; everything else
* (drawing the grid, reading keys, hit testing) is the complete pattern.
*
* 界面文字跟着宿主语言走(`ui_scr_get_language()`),与 `demo_ui.c` 同一套做法。
* The on-screen text follows the host language (`ui_scr_get_language()`), the same approach `demo_ui.c` uses.
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
#include <time.h> /* time / localtime(`tm_t` 与 C 的 `struct tm` 同 layout) */
/* time / localtime (`tm_t` has the same layout as C's `struct tm`) */
#define IDLE_EXIT_MS 8000
static int SW; /* 画布宽 */
/* canvas width */
static int SH; /* 画布高 */
/* canvas height */
/* 界面语言:0 = 中文,1 = English */
/* UI language: 0 = Chinese, 1 = English */
static int lang;
/* 配色(真彩 AA RRGGBB)*/
/* Colours (true colour AA RRGGBB) */
#define C_BG 0xFF101018
#define C_CARD 0xFF1A1A24
#define C_TITLE 0xFFE8E8F0
#define C_DIM 0xFF9AA0B0
#define C_ACCENT 0xFF4A90D9
#define C_TODAY 0xFFFFC64A
#define C_WEEKEND 0xFFE06C50
#define C_DAY 0xFFD8DCE8
/* 是不是闰年 */
/* Is it a leap year */
static int is_leap(int y) {
if (y % 400 == 0) return 1;
if (y % 100 == 0) return 0;
return (y % 4 == 0) ? 1 : 0;
}
/* 这个月有几天 */
/* How many days this month has */
static int days_in_month(int y, int m) {
if (m == 2) return is_leap(y) ? 29 : 28;
if (m == 4 || m == 6 || m == 9 || m == 11) return 30;
return 31;
}
/* 某月 1 号是星期几(0=周日)。蔡勒公式,编号与 `localtime` 的 `tm_wday` 一致。
* Weekday of the 1st of a month (0 = Sunday). Zeller's congruence, numbered like `localtime`'s `tm_wday`. */
static int weekday_of_first(int y, int m) {
int yy = y;
int mm = m;
int k;
int j;
int h;
if (mm < 3) { mm = mm + 12; yy = yy - 1; } /* 1、2 月算作上一年的 13、14 月 */
/* Jan and Feb count as months 13 and 14 of the year before */
k = yy % 100;
j = yy / 100;
h = (1 + (13 * (mm + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7; /* h: 0=周六 */
/* 0 = Saturday */
return (h + 6) % 7; /* 换成 0=周日 */
/* to 0 = Sunday */
}
static char *month_en(int m) {
if (m == 1) return "January";
if (m == 2) return "February";
if (m == 3) return "March";
if (m == 4) return "April";
if (m == 5) return "May";
if (m == 6) return "June";
if (m == 7) return "July";
if (m == 8) return "August";
if (m == 9) return "September";
if (m == 10) return "October";
if (m == 11) return "November";
return "December";
}
/* 整数 → 十进制串(`ui_2d_text` 只收字符串;第 4 层没有 `print_int` 那种东西)
* Integer to a decimal string (`ui_2d_text` takes strings only; layer 4 has no `print_int`). */
static char numbuf[16];
static char *numstr(int v) {
itoa(v, numbuf);
return numbuf;
}
/* 画两位日号(补零,7×7 的格子里对齐才好看)
* A two-digit day number (zero padded, which keeps the 7x7 grid aligned) */
static char daybuf[4];
static char *daystr(int d) {
daybuf[0] = (char)('0' + (d / 10) % 10);
daybuf[1] = (char)('0' + d % 10);
daybuf[2] = 0;
return daybuf;
}
/* 当前显示的月份 */
/* The month currently on screen */
static int cur_y;
static int cur_m;
/* 选中/高亮的日号,0 = 没有 */
/* The selected (highlighted) day, 0 = none */
static int sel_d;
static int today_y;
static int today_m;
static int today_d;
/* 棋盘几何(画与命中判定**必须用同一份**,见 `hit_day`)
* Grid geometry (drawing and hit testing **must share this one copy**, see `hit_day`) */
static int g_pad;
static int g_cell;
static int g_top;
static void layout(void) {
int grid_h;
int block_h;
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 360;
if (SH <= 0) SH = 620;
g_pad = SW / 18;
if (g_pad < 6) g_pad = 6;
g_cell = (SW - g_pad * 2) / 7; /* 7 列等宽;余数留在右边不动 */
/* 7 equal columns; any remainder stays on the right */
if (g_cell < 18) g_cell = 18;
/* ⚠ **行数固定 6**:月历最长 6 行(31 天且 1 号在周六时)。按"这个月实际几行"算的话,
* ⚠ **Always six rows**: a month spans at most six rows (31 days with the 1st on a Saturday).
* 翻月时整块会上下跳(实测过:5 行与 6 行来回切,日历像在抖)。
* Sizing by the actual row count makes the whole block jump when you change month (measured:
* flipping between five and six rows makes the calendar visibly shake).
*/
grid_h = 6 * g_cell;
block_h = 64 + grid_h; /* 标题 + 表头(64)+ 网格 */
/* title + header (64) + grid */
g_top = (SH - block_h) / 2 + 64; /* 整块垂直居中 */
/* centre the whole block vertically */
if (g_top < 80) g_top = 80; /* 矮屏兜底:别把标题顶出画布 */
/* short screens: keep the title on the canvas */
}
/* 第 d 天那一格的左上角 */
/* Top-left corner of the cell for day `d` */
static int cell_x(int idx) { return g_pad + (idx % 7) * g_cell; }
static int cell_y(int idx) { return g_top + (idx / 7) * g_cell; }
static void draw(void) {
int first = weekday_of_first(cur_y, cur_m);
int n = days_in_month(cur_y, cur_m);
int cx = SW / 2;
int i;
int d;
int idx;
int x;
int y;
ui_2d_clear(C_BG);
/* 标题 */
/* Title */
/* ⚠ **中文一律走字符串字面量拼接,绝不用 `buf[k] = '年'` 这种逐字节赋值** —— 一个 `char` 只装得下 */
/* ⚠ **Append Chinese text as a string literal, never as a byte-by-byte assignment** -- one `char` holds */
/* 一个字节,而一个汉字在 UTF-8 里是 3 个字节,写进去就是截断(实测:标题显示成 `2026 t 10 □`)。*/
/* a single byte while one CJK glyph is 3 bytes in UTF-8, so it truncates (measured: `2026 t 10 □`).*/
{
char buf[48];
buf[0] = 0;
if (lang == 0) {
strcat(buf, numstr(cur_y));
strcat(buf, " 年 ");
strcat(buf, numstr(cur_m));
strcat(buf, " 月");
} else {
strcat(buf, month_en(cur_m));
strcat(buf, " ");
strcat(buf, numstr(cur_y));
}
ui_2d_text_styled(cx, g_top - 64, buf, C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD);
}
/* 星期表头:一格一个词,位置与下面的格子**同一套算式**
* Weekday header: one word per cell, positioned by the **same formula** the cells below use */
for (i = 0; i < 7; i++) {
char *w;
if (lang == 0) {
w = (i == 0) ? "日" : (i == 1) ? "一" : (i == 2) ? "二" : (i == 3) ? "三"
: (i == 4) ? "四" : (i == 5) ? "五" : "六";
} else {
w = (i == 0) ? "Su" : (i == 1) ? "Mo" : (i == 2) ? "Tu" : (i == 3) ? "We"
: (i == 4) ? "Th" : (i == 5) ? "Fr" : "Sa";
}
ui_2d_text(g_pad + i * g_cell + g_cell / 2, g_top - 28, w,
(i == 0 || i == 6) ? C_WEEKEND : C_DIM, 13, VML_ANCHOR_CENTER);
}
/* 日期格:先铺底,再写日号 */
/* Day cells: background first, then the number */
for (d = 1; d <= n; d++) {
int is_today;
int is_sel;
idx = first + d - 1;
x = cell_x(idx);
y = cell_y(idx);
is_today = (cur_y == today_y && cur_m == today_m && d == today_d);
is_sel = (d == sel_d);
if (is_sel) {
ui_2d_rect(x + 3, y + 3, g_cell - 6, g_cell - 6, C_ACCENT, 1, 0, g_cell / 4);
} else if (is_today) {
ui_2d_rect(x + 3, y + 3, g_cell - 6, g_cell - 6, C_TODAY, 1, 0, g_cell / 4);
} else {
ui_2d_rect(x + 3, y + 3, g_cell - 6, g_cell - 6, C_CARD, 1, 0, g_cell / 4);
}
ui_2d_text(x + g_cell / 2, y + g_cell / 2 - 7, daystr(d),
(is_sel || is_today) ? 0xFF101018 : ((idx % 7 == 0 || idx % 7 == 6) ? C_WEEKEND : C_DAY),
15, VML_ANCHOR_CENTER);
}
/* 底部:高亮说明 + 操作提示 */
/* Bottom: what is highlighted, and the controls */
{
int by = g_top + ((first + n - 1) / 7 + 1) * g_cell + 12;
if (by > SH - 40) by = SH - 40;
if (sel_d > 0) {
ui_2d_text(cx, by, lang == 0 ? "已选中(点一天 / 用方向键翻月)" : "selected (tap a day / arrows change month)",
C_ACCENT, 12, VML_ANCHOR_CENTER);
} else {
ui_2d_text(cx, by, lang == 0 ? "黄底 = 今天" : "yellow = today", C_TODAY, 12, VML_ANCHOR_CENTER);
}
ui_2d_text(cx, by + 22, lang == 0 ? "← → 翻月 ↑ ↓ 翻年 点一天选中 Esc 退出"
: "Left/Right month Up/Down year tap a day Esc quits",
C_DIM, 11, VML_ANCHOR_CENTER);
}
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* without this call you get a black screen */
}
/* 点在某一天的格子里?返回日号,没点中返回 0。
* Did the tap land in a day cell? Returns the day number, or 0 when it missed.
* ⚠ **必须与 `draw` 用同一套几何**(`cell_x/cell_y`):各算一套就会出现"看着点在 7 号、
* ⚠ **It must use the same geometry as `draw`** (`cell_x/cell_y`): two sets of arithmetic produce
* 实际选中 8 号"这种"意图在变、位置不动"的老毛病。
* the classic "it looks like you tapped the 7th but the 8th got selected" bug.
*/
static int hit_day(int px, int py) {
int first = weekday_of_first(cur_y, cur_m);
int n = days_in_month(cur_y, cur_m);
int d;
int idx;
int x;
int y;
for (d = 1; d <= n; d++) {
idx = first + d - 1;
x = cell_x(idx);
y = cell_y(idx);
if (px >= x && px < x + g_cell && py >= y && py < y + g_cell) return d;
}
return 0;
}
static void shift_month(int delta) {
cur_m = cur_m + delta;
if (cur_m < 1) { cur_m = 12; cur_y = cur_y - 1; }
if (cur_m > 12) { cur_m = 1; cur_y = cur_y + 1; }
sel_d = 0;
}
int main(void) {
int msg[4];
int t;
int t0;
int done;
lang = ui_scr_get_language();
/* 今天:`time`/`localtime` 来自 `util`,`tm_t` 的字段与 C 的 `struct tm` 一致 */
/* Today: `time`/`localtime` come from `util` and `tm_t` matches C's `struct tm` */
{
time_t now = time(0);
tm_t *tm = localtime(&now);
cur_y = tm->tm_year + 1900;
cur_m = tm->tm_mon + 1;
today_y = cur_y;
today_m = cur_m;
today_d = tm->tm_mday;
}
sel_d = 0;
/* 开窗:**传 `0, 0`(尺寸交给宿主),开窗之后**再取一次尺寸 —— 那一次才是**真实画布**,
* Open the window: **pass `0, 0` (let the host size it) and take the size again after the open** -- that pass is the **real canvas**,
* 窗口与画布才是一套坐标系(开窗前的 `layout()` 只是先算一版)。
* so window and canvas share one coordinate space (the pre-open `layout()` is just a first pass).
* 竖屏 + 不要手柄区(日历全靠点,用不到摇杆)。可旋转。
* Portrait plus no gamepad area (a calendar is all tapping, no stick needed). Rotatable. */
layout();
ui_win_open_ex("calendar.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout();
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 (for automated checks) */
continue;
}
t0 = ui_time_get_ms();
if (t == VML_MSG_KEYDOWN) {
int k = msg[1];
if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
else if (k == VML_KEY_LEFT) { shift_month(-1); draw(); }
else if (k == VML_KEY_RIGHT) { shift_month(1); draw(); }
else if (k == VML_KEY_UP) { cur_y = cur_y - 1; sel_d = 0; draw(); }
else if (k == VML_KEY_DOWN) { cur_y = cur_y + 1; sel_d = 0; draw(); }
continue;
}
if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
int d = hit_day(msg[1], msg[2]);
if (d > 0) { sel_d = d; draw(); }
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
layout();
draw();
continue;
}
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
return 0;
}