Files

395 lines
15 KiB
C
Raw Permalink Normal View History

/* startmenu.c — M3 Win11 开始菜单
*
* 任务栏开始按钮弹出:搜索框 + "已固定" 应用瓦片网格 + 底部用户/电源行。
* 条目来自 appreg.c 的 g_sm_ids/g_sm_labels(STARTMENU.CNF,缺失回退全量)。
*
* 布局(浅色主题,深色壁纸衬托亚克力):
* 面板 520 × min(600, H-128),锚在开始按钮上方
* [搜索框 36px] [已固定 标签] [瓦片网格 6 列 × N 行]
* [分隔线] [用户 | 电源]
*
* 交互:
* 搜索:ASCII 输入实时过滤(display/name 不区分大小写子串),
* 过滤态切 du_wf_list_item 列表,Up/Down + Enter 键盘启动
* 瓦片:点击 → launch_app + 关闭
* 电源:g_quit(与托盘电源一致);Esc/点击面板外关闭
*
* 绘制挂在 taskbar_draw 末尾(叠加在任务栏之上);
* 点击/键盘经 taskbar_click / handle_key 路由(模态,参考 WiFi 卡片)。
*/
#include "desktop.h"
#include "ascii_font.h" /* 顺序敏感:先于 fluent_ui.h */
#include "../UTSM/include/utsm/fluent_ui.h"
#include "zhfont.h"
/* ---- 全局状态 ---- */
int g_sm_open = 0;
#define SM_PANEL_W 520
#define SM_PANEL_MIN_H 380
#define SM_SEARCH_MAX 28 /* 搜索串字节上限(ASCII) */
#define SM_COLS 6
#define SM_TILE_H 84
#define SM_TILE_GAP 4
#define SM_GRID_PAD 16
#define SM_BOTTOM_H 56
static char g_sm_search[SM_SEARCH_MAX + 1];
static int g_sm_search_len;
static int g_sm_sel; /* 过滤列表选中行 */
static int g_sm_panel_rect[4];
static int g_sm_search_rect[4];
static int g_sm_tile_rect[SM_MAX_APPS][4];
static int g_sm_pwr_rect[4];
static int g_sm_user_rect[4];
/* 过滤结果(g_sm 下标),-1 结尾 */
static int g_sm_filter[SM_MAX_APPS + 1];
static int g_sm_filter_n;
/* 网格实际显示的条目数(容量截断时串口留档) */
static int g_sm_grid_shown;
static int g_sm_grid_dropped;
/* 当前模式实际绘制(可点击)的行数:grid=瓦片数 / list=过滤行数 */
static int g_sm_rows_visible;
static const wf_theme *sm_theme(void) { return &g_wf_light; }
static wf_btn_state sm_hover_state(const int r[4]) {
if (g_mouse_x >= r[0] && g_mouse_x < r[0] + r[2] &&
g_mouse_y >= r[1] && g_mouse_y < r[1] + r[3])
return WF_BTN_HOVER;
return WF_BTN_NORMAL;
}
/* ============================================================
* 开 / 关 / 过滤
* ============================================================ */
static void sm_filter_rebuild(void) {
g_sm_filter_n = 0;
for (int i = 0; i < g_sm_count && g_sm_filter_n < SM_MAX_APPS; i++) {
if (g_sm_search_len == 0) {
g_sm_filter[g_sm_filter_n++] = i;
continue;
}
/* 不区分大小写 ASCII 子串匹配:display 标签 + 内部名 */
const char *hay[2] = { g_sm_labels[i],
g_apps[g_sm_ids[i]].name };
int hit = 0;
for (int h = 0; h < 2 && !hit; h++) {
const char *s = hay[h];
if (!s) continue;
for (int b = 0; s[b] && !hit; b++) {
int m = 1;
for (int k = 0; k < g_sm_search_len; k++) {
char c1 = s[b + k], c2 = g_sm_search[k];
if (!c1) { m = 0; break; }
if (c1 >= 'a' && c1 <= 'z') c1 = (char)(c1 - 32);
if (c2 >= 'a' && c2 <= 'z') c2 = (char)(c2 - 32);
if (c1 != c2) { m = 0; break; }
}
if (m) hit = 1;
}
}
if (hit) g_sm_filter[g_sm_filter_n++] = i;
}
g_sm_filter[g_sm_filter_n] = -1;
if (g_sm_sel >= g_sm_filter_n) g_sm_sel = 0;
}
void startmenu_open(void) {
g_sm_open = 1;
g_sm_search[0] = 0;
g_sm_search_len = 0;
g_sm_sel = 0;
sm_filter_rebuild();
g_wifi_open = 0; /* 互斥:收起托盘弹卡 */
g_qs_open = 0;
slog("[startmenu] open");
}
void startmenu_close(void) {
if (!g_sm_open) return;
g_sm_open = 0;
slog("[startmenu] close");
}
void startmenu_toggle(void) {
if (g_sm_open) startmenu_close();
else startmenu_open();
}
/* 瓦片标签截断:zh 16px ASCII 半宽 12 / CJK 全宽 16 → 瓦片 ~80px 最多 4 个 CJK */
static void sm_label_clip(char *dst, int cap, const char *src) {
int w = 0, o = 0;
for (int i = 0; src[i] && o < cap - 4; ) {
unsigned char c = (unsigned char)src[i];
int cw = (c < 0x80) ? 12 : 16; /* ASCII 半宽 12px / CJK 全宽 16px */
if (w + cw > 68) break;
dst[o++] = src[i++];
if (c >= 0x80) { /* UTF-8 续字节整组拷贝 */
int cont = (c >= 0xF0) ? 3 : (c >= 0xE0) ? 2 : 1;
while (cont-- > 0 && src[i] && o < cap - 4)
dst[o++] = src[i++];
}
w += cw;
}
dst[o] = 0;
}
/* ============================================================
* 绘制(taskbar_draw 末尾调用)
* ============================================================ */
void startmenu_draw(void) {
if (!g_sm_open) return;
const wf_theme *t = sm_theme();
int W = (int)g_fb_w, H = (int)g_fb_h;
/* ---- 面板几何:开始按钮上方 ---- */
int ph = H - 64 - 8 - 24; /* 任务栏(48+8gap) 上方 8 间隙 + 顶部余量 */
if (ph > 600) ph = 600;
if (ph < SM_PANEL_MIN_H) ph = SM_PANEL_MIN_H;
int px = g_tb2_start_rect[0] - 8;
if (px < 8) px = 8;
if (px + SM_PANEL_W > W - 8) px = W - 8 - SM_PANEL_W;
int py = H - 64 - ph; /* 任务栏区(56) 之上贴住 */
if (py < 8) py = 8;
g_sm_panel_rect[0] = px; g_sm_panel_rect[1] = py;
g_sm_panel_rect[2] = SM_PANEL_W; g_sm_panel_rect[3] = ph;
/* ---- 卡片底 ---- */
du_wf_shadow(&g_fb, px, py, SM_PANEL_W, ph, (du_u32)t->radius_lg, t);
du_wf_acrylic(&g_fb, px, py, SM_PANEL_W, ph, 235, (du_u32)t->radius_lg, t);
/* ---- 搜索框 ---- */
g_sm_search_rect[0] = px + 16; g_sm_search_rect[1] = py + 16;
g_sm_search_rect[2] = SM_PANEL_W - 32; g_sm_search_rect[3] = 36;
{
/* blink:RTC 秒奇偶交替 */
int yy, mo, dd, hh, mi, ss;
rtc_read(&yy, &mo, &dd, &hh, &mi, &ss);
du_wf_textbox(&g_fb, px + 16, py + 16, SM_PANEL_W - 32,
g_sm_search[0] ? g_sm_search : "search",
ss & 1, t, 1);
if (!g_sm_search[0])
du_wf_icon(&g_fb, WF_ICON_SEARCH,
px + SM_PANEL_W - 40, py + 22, 22, t->text_dim);
}
/* ---- 过滤列表模式(有搜索串)---- */
if (g_sm_search_len > 0) {
zh_draw_string(&g_fb, "搜索结果", px + 16, py + 66,
t->text_dim, 0);
int y = py + 92;
int max_rows = (py + ph - SM_BOTTOM_H - y) / 36;
if (max_rows > SM_MAX_APPS) max_rows = SM_MAX_APPS;
int rows = g_sm_filter_n;
if (rows > max_rows) rows = max_rows;
g_sm_rows_visible = rows;
for (int i = 0; i < rows; i++) {
int e = g_sm_filter[i];
int id = g_sm_ids[e];
char label[52];
sm_label_clip(label, 52, g_sm_labels[e]);
/* zh 列表项:底行 + 名字(ASCII name 右侧小字) */
int rowy = y + i * 36;
if (g_sm_sel == i)
du_wf_overlay(&g_fb, px + 8, rowy, SM_PANEL_W - 16, 36, 0x28000000u);
else if (sm_hover_state((int[4]){px + 8, rowy, SM_PANEL_W - 16, 36})
== WF_BTN_HOVER)
du_wf_overlay(&g_fb, px + 8, rowy, SM_PANEL_W - 16, 36, 0x14000000u);
if (g_sm_sel == i)
du_fill_rect(&g_fb, px + 8, rowy + 8, 3, 20, t->accent);
du_wf_icon(&g_fb, (wf_icon_id)desktop_app_icon(id),
px + 20, rowy + 7, 22, t->text_primary);
zh_draw_string(&g_fb, label, px + 54, rowy + 10,
t->text_primary, 0);
du_draw_string(&g_fb, g_apps[id].name,
px + SM_PANEL_W - 16 - kstrlen(g_apps[id].name) * 8,
rowy + 12, t->text_dim, 0, DU_ASCII_STEP);
/* 行命中矩形(点击用) */
g_sm_tile_rect[i][0] = px + 8; g_sm_tile_rect[i][1] = rowy;
g_sm_tile_rect[i][2] = SM_PANEL_W - 16; g_sm_tile_rect[i][3] = 36;
}
if (g_sm_filter_n == 0)
zh_draw_string(&g_fb, "无匹配应用", px + 24, y + 8, t->text_dim, 0);
goto bottom_bar;
}
/* ---- 瓦片网格模式 ---- */
zh_draw_string(&g_fb, "已固定", px + 16, py + 66, t->text_dim, 0);
{
int grid_y = py + 92;
int grid_h = py + ph - SM_BOTTOM_H - grid_y;
int rows_cap = grid_h / (SM_TILE_H + SM_TILE_GAP);
int cap = rows_cap * SM_COLS;
if (cap > SM_MAX_APPS) cap = SM_MAX_APPS;
g_sm_grid_shown = g_sm_count;
if (g_sm_grid_shown > cap) {
g_sm_grid_dropped = g_sm_count - cap;
g_sm_grid_shown = cap;
} else {
g_sm_grid_dropped = 0;
}
g_sm_rows_visible = g_sm_grid_shown;
int tile_w = (SM_PANEL_W - SM_GRID_PAD * 2 - (SM_COLS - 1) * SM_TILE_GAP)
/ SM_COLS;
for (int i = 0; i < g_sm_grid_shown; i++) {
int col = i % SM_COLS, row = i / SM_COLS;
int tx = px + SM_GRID_PAD + col * (tile_w + SM_TILE_GAP);
int ty = grid_y + row * (SM_TILE_H + SM_TILE_GAP);
g_sm_tile_rect[i][0] = tx; g_sm_tile_rect[i][1] = ty;
g_sm_tile_rect[i][2] = tile_w; g_sm_tile_rect[i][3] = SM_TILE_H;
int id = g_sm_ids[i];
du_wf_app_tile(&g_fb, tx, ty, tile_w, SM_TILE_H,
(wf_icon_id)desktop_app_icon(id), t,
sm_hover_state(g_sm_tile_rect[i]), 0);
char label[52];
sm_label_clip(label, 52, g_sm_labels[i]);
zh_draw_string(&g_fb, label,
tx + (tile_w - zh_string_width(label, 0)) / 2,
ty + SM_TILE_H - 30, t->text_primary, 0);
}
if (g_sm_grid_dropped > 0) {
char buf[24];
buf[0] = '+';
int n = u32dec((u32)g_sm_grid_dropped, buf + 1);
buf[n + 1] = 0;
zh_draw_string(&g_fb, buf, px + SM_PANEL_W - 60,
py + 66, t->text_dim, 0);
}
}
bottom_bar:
/* ---- 底部:分隔线 + 用户 + 电源 ---- */
{
int by = py + ph - SM_BOTTOM_H;
du_wf_separator(&g_fb, px + 16, by, SM_PANEL_W - 32, t);
/* 用户行 */
g_sm_user_rect[0] = px + 12; g_sm_user_rect[1] = by + 8;
g_sm_user_rect[2] = 220; g_sm_user_rect[3] = 40;
du_wf_icon_button(&g_fb, px + 12, by + 8, 40, t,
sm_hover_state(g_sm_user_rect));
du_wf_icon(&g_fb, WF_ICON_USER, px + 20, by + 16, 24, t->text_primary);
zh_draw_string(&g_fb, "Deshab 用户", px + 60, by + 16,
t->text_primary, 0);
/* 电源钮 */
g_sm_pwr_rect[0] = px + SM_PANEL_W - 52; g_sm_pwr_rect[1] = by + 8;
g_sm_pwr_rect[2] = 40; g_sm_pwr_rect[3] = 40;
du_wf_icon_button(&g_fb, px + SM_PANEL_W - 52, by + 8, 40, t,
sm_hover_state(g_sm_pwr_rect));
du_wf_icon(&g_fb, WF_ICON_POWER, px + SM_PANEL_W - 44, by + 16, 24,
t->text_primary);
}
}
/* ============================================================
* 点击(taskbar_click 调用;返回 1 = 已消费)
* ============================================================ */
int startmenu_click(int mx, int my) {
if (!g_sm_open) return 0;
if (!rect_hit(g_sm_panel_rect, mx, my)) {
startmenu_close(); /* 点面板外:关闭并放行点击 */
return 0;
}
/* 电源 → 退出桌面(与托盘电源一致) */
if (rect_hit(g_sm_pwr_rect, mx, my)) {
startmenu_close();
g_quit = 1;
return 1;
}
/* 用户行(预留:M7 用户菜单) */
if (rect_hit(g_sm_user_rect, mx, my)) return 1;
/* 瓦片 / 过滤行 → 启动(只认实际绘制的行,防越界脏矩形) */
int n = g_sm_rows_visible;
for (int i = 0; i < n; i++) {
if (!rect_hit(g_sm_tile_rect[i], mx, my)) continue;
int e = g_sm_filter[i];
slog("[startmenu] launch:");
slog(g_apps[g_sm_ids[e]].name);
startmenu_close();
launch_app(g_sm_ids[e]);
return 1;
}
return 1; /* 面板内空白:吞掉 */
}
/* ============================================================
* 键盘(handle_key 调用;返回 1 = 已消费)
* ============================================================ */
int startmenu_key(u8 sc, int shift) {
if (!g_sm_open) return 0;
if (sc == 0x01) { startmenu_close(); return 1; } /* Esc */
if (sc == 0x0E) { /* Backspace */
if (g_sm_search_len > 0) {
g_sm_search[--g_sm_search_len] = 0;
sm_filter_rebuild();
}
return 1;
}
if (sc == 0x1C) { /* Enter 启动选中 */
if (g_sm_search_len > 0 && g_sm_filter_n > 0) {
int e = g_sm_filter[g_sm_sel];
slog("[startmenu] launch(kbd):");
slog(g_apps[g_sm_ids[e]].name);
startmenu_close();
launch_app(g_sm_ids[e]);
} else if (g_sm_grid_shown > 0) {
int id = g_sm_ids[0];
startmenu_close();
launch_app(id);
}
return 1;
}
if (g_sm_search_len > 0) { /* 过滤列表导航 */
if (sc == 0x48) { /* Up */
if (g_sm_sel > 0) g_sm_sel--;
return 1;
}
if (sc == 0x50) { /* Down */
if (g_sm_sel < g_sm_filter_n - 1) g_sm_sel++;
return 1;
}
}
/* 可打印字符 → 搜索串 */
if (sc >= 58) return 1;
{
static const char normal[58] = {
0, 27, '1','2','3','4','5','6','7','8','9','0','-','=', 8, '\t',
'q','w','e','r','t','y','u','i','o','p','[',']','\n',0,
'a','s','d','f','g','h','j','k','l',';','\'', '`',0,'\\',
'z','x','c','v','b','n','m',',','.','/',0,'*',0,' '
};
static const char shifted[58] = {
0, 27, '!','@','#','$','%','^','&','*','(',')','_','+', 8, '\t',
'Q','W','E','R','T','Y','U','I','O','P','{','}','\n',0,
'A','S','D','F','G','H','J','K','L',':','"','~',0,'|',
'Z','X','C','V','B','N','M','<','>','?',0,'*',0,' '
};
char c = shift ? shifted[sc] : normal[sc];
if (c && c != '\n' && c != 27 && c != 8 && c != '\t') {
if (g_sm_search_len < SM_SEARCH_MAX) {
g_sm_search[g_sm_search_len++] = c;
g_sm_search[g_sm_search_len] = 0;
g_sm_sel = 0;
sm_filter_rebuild();
}
}
}
return 1;
}