feat(window): 标题栏 hover、最大化图标、边缘缩放与控件光标

1) 标题栏按钮 hover:min/max 背景按 alpha 压暗,close 变红、图标变白;
   按下态更深。新增 hover_btn_win/kind,仅在变化时重画标题栏。
2) 最大化图标改为单线方框(上边框不再两行加厚);最大化后显示
   两个错位重叠方框作为“还原”。
3) 窗口边缘 5px 热区支持拖拽缩放:左/右 ↔、上/下 ↕、四角斜向;
   缩放中实时更新几何与内容缓冲(按 1.5 倍扩容,避免 arena 抖动),
   并向 app 发 RESIZE。
4) 新增 DESK_REQ_CURSOR / window.set_cursor_type 真正下发光标类型;
   explorer 按窗口 cursor + 边缘热区选择箭头 / I 形 / 手指 / 缩放光标。
5) vpui.Tk 在 HOVER 时按最上层控件设置光标:Entry/Text → I 形,
   Button → 手指;Entry 自己的光标请求移除,避免与集中逻辑冲突。
This commit is contained in:
TermiNexus committed 2026-10-27 13:14:42 +08:00
1 parent 6ecd891f64
commit b9efc30d71
4 files changed
+422 -41

No files matched your search

+391 -33
View File
@@ -49,6 +49,18 @@ define STYLE_WIN10 = 1
define STYLE_WIN11 = 2
define STYLE_WIN7 = 3
define STYLE_WINXP = 4
# 光标形状(与 vpsdk/window.vp 的 CURSOR_* 对齐)
define CURSOR_DEFAULT = 0
define CURSOR_IBEAM = 1
define CURSOR_RESIZE_EW = 2
define CURSOR_RESIZE_NS = 3
define CURSOR_RESIZE_NWSE = 4
define CURSOR_HAND = 5
define CURSOR_RESIZE_NESW = 6
# 窗口边缘拖拽缩放的热区宽度 / 最小外框尺寸
define RESIZE_BORDER = 5
define MIN_WIN_W = 160
define MIN_WIN_H = 100
# 窗口阴影(0 = 关闭)
define SHADOW_ENABLED = 1
define SHADOW_LEFT = 8
@@ -78,6 +90,120 @@ _CURSOR_ARROW: [256]uint8 = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
]
# 其余光标模板同样是 16×16、0=透明 1=黑边 2=白心。
_CURSOR_IBEAM: [256]uint8 = [
0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,
0,0,0,0,0,1,2,2,2,2,1,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,1,2,2,2,2,1,0,0,0,0,0,
0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,
]
_CURSOR_HAND: [256]uint8 = [
0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,1,1,1,0,0,0,0,
0,0,0,0,0,1,2,2,2,2,2,2,1,1,0,0,
0,0,0,0,1,2,1,2,2,1,2,2,2,1,0,0,
0,0,0,1,2,2,1,2,2,1,2,2,2,1,0,0,
0,0,1,2,2,2,1,2,2,1,2,2,2,1,0,0,
0,0,1,2,2,2,1,2,2,1,2,2,1,0,0,0,
0,0,1,2,2,2,1,2,2,1,2,2,1,0,0,0,
0,0,0,1,2,2,2,2,2,2,2,2,1,0,0,0,
0,0,0,1,2,2,2,2,2,2,2,1,0,0,0,0,
0,0,0,0,1,1,2,2,2,2,2,1,0,0,0,0,
0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,
]
_CURSOR_EW: [256]uint8 = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,
0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,
0,0,1,1,2,0,0,0,0,0,0,2,1,1,0,0,
0,1,1,2,2,1,1,1,1,1,1,2,2,1,1,0,
1,1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,
0,1,1,2,2,2,2,2,2,2,2,2,2,1,1,0,
0,0,1,1,2,1,1,1,1,1,1,2,1,1,0,0,
0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,
0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
]
_CURSOR_NS: [256]uint8 = [
0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,
0,0,0,0,0,1,1,2,1,1,0,0,0,0,0,0,
0,0,0,0,1,1,2,2,2,1,1,0,0,0,0,0,
0,0,0,1,1,2,2,2,2,2,1,1,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,
0,0,0,1,1,2,2,2,2,2,1,1,0,0,0,0,
0,0,0,0,1,1,2,2,2,1,1,0,0,0,0,0,
0,0,0,0,0,1,1,2,1,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
]
_CURSOR_NWSE: [256]uint8 = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,
0,1,1,2,2,2,0,0,0,0,0,0,0,0,0,0,
0,0,1,2,2,1,1,0,0,0,0,0,0,0,0,0,
0,0,1,2,1,2,1,1,0,0,0,0,0,0,0,0,
0,0,1,0,1,1,2,1,1,0,0,0,0,0,0,0,
0,0,0,0,0,1,1,2,1,1,0,0,0,0,0,0,
0,0,0,0,0,0,1,1,2,1,1,0,0,0,0,0,
0,0,0,0,0,0,0,1,1,2,1,1,0,1,0,0,
0,0,0,0,0,0,0,0,1,1,2,1,2,1,0,0,
0,0,0,0,0,0,0,0,0,1,1,2,2,1,0,0,
0,0,0,0,0,0,0,0,0,0,2,2,2,1,1,0,
0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
]
_CURSOR_NESW: [256]uint8 = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,
0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0,0,0,2,2,2,1,1,0,
0,0,0,0,0,0,0,0,0,1,1,2,2,1,0,0,
0,0,0,0,0,0,0,0,1,1,2,1,2,1,0,0,
0,0,0,0,0,0,0,1,1,2,1,1,0,1,0,0,
0,0,0,0,0,0,1,1,2,1,1,0,0,0,0,0,
0,0,0,0,0,1,1,2,1,1,0,0,0,0,0,0,
0,0,1,0,1,1,2,1,1,0,0,0,0,0,0,0,
0,0,1,2,1,2,1,1,0,0,0,0,0,0,0,0,
0,0,1,2,2,1,1,0,0,0,0,0,0,0,0,0,
0,1,1,2,2,2,0,0,0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,
0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
]
# 圆角窗口的角点/边框判定(移植自内核桌面 desktop.vp)
def _is_in_corner(px: int, py: int, cx: int, cy: int, r: int) -> int:
@@ -164,6 +290,18 @@ class DeskServer:
# 按住鼠标时的捕获窗口:内容区按下后,移动/松开事件始终投给它(滚动条等
# 控件靠按住期间的 HOVER 更新位置;移出窗口也不能丢)。
capture_win: int
# 边缘拖拽缩放:窗口下标、边掩码(1左 2右 4上 8下)、起始几何/指针
resize_win: int
resize_edge: int
resize_ox: int
resize_oy: int
resize_ow: int
resize_oh: int
resize_sx: int
resize_sy: int
# 标题栏按钮的 hover 状态(用于变色和刷新)
hover_btn_win: int
hover_btn_kind: int
# 复用缓冲
msg: desk.DeskMsg
evt: desk.DeskMsg
@@ -213,6 +351,16 @@ class DeskServer:
self.drag_dx = 0
self.drag_dy = 0
self.capture_win = -1
self.resize_win = -1
self.resize_edge = 0
self.resize_ox = 0
self.resize_oy = 0
self.resize_ow = 0
self.resize_oh = 0
self.resize_sx = 0
self.resize_sy = 0
self.hover_btn_win = -1
self.hover_btn_kind = 0
# 预构造一行背景,渲染时整行 memcpy
self.bg_row = uint32*(syscall.mmap(0, uint64(w) * uint64(4),
uint64(syscall.PROT_READ | syscall.PROT_WRITE),
@@ -291,6 +439,18 @@ class DeskServer:
base: int = y * self.fb_w
string.memset32(rawptr(uint64(self.back) + uint64((base + x) * 4)), col, uint64(dw))
# 把一行中 [x0, x0+w) 与黑色按 alpha 混合,用于按钮 hover 的“稍微变深”。
def _dim_row(self, x0: int, y: int, w: int, alpha: int):
if y < 0 or y >= self.fb_h or w <= 0: return
x: int = x0
if x < 0: x = 0
xe: int = x0 + w
if xe > self.fb_w: xe = self.fb_w
base: int = y * self.fb_w
while x < xe:
self.back[base + x] = self._blend(self.back[base + x], uint32(0), alpha)
x += 1
def row_blit(self, x0: int, y: int, src: uint32*, src_off: int, w: int):
"""把 src[src_off .. src_off+w) 拷到屏幕 (x0, y),裁剪到屏幕内。"""
if y < 0 or y >= self.fb_h or w <= 0: return
@@ -317,6 +477,34 @@ class DeskServer:
if ch < 1: ch = 1
return ch
# 指针落在窗口哪条边(1左 2右 4上 8下,可组合成角);0 = 不在边缘/最大化
def _resize_edge_at(self, s: desk.WinSlot*, x: int, y: int) -> int:
if s.maximized != 0: return 0
if x < s.ux or x >= s.ux + s.w or y < s.uy or y >= s.uy + s.h: return 0
e: int = 0
if x < s.ux + RESIZE_BORDER: e |= 1
if x >= s.ux + s.w - RESIZE_BORDER: e |= 2
if y < s.uy + RESIZE_BORDER: e |= 4
if y >= s.uy + s.h - RESIZE_BORDER: e |= 8
# 标题栏按钮区域不参与缩放(否则按钮上沿会被当成顶边)
if e != 0 and self._title_btn_hit(s, x, y) != 0: return 0
return e
def _edge_cursor(self, e: int) -> int:
if e == 0: return CURSOR_DEFAULT
if (e & 1) != 0 and (e & 4) != 0: return CURSOR_RESIZE_NWSE
if (e & 2) != 0 and (e & 8) != 0: return CURSOR_RESIZE_NWSE
if (e & 2) != 0 and (e & 4) != 0: return CURSOR_RESIZE_NESW
if (e & 1) != 0 and (e & 8) != 0: return CURSOR_RESIZE_NESW
if (e & 3) != 0: return CURSOR_RESIZE_EW
if (e & 12) != 0: return CURSOR_RESIZE_NS
return CURSOR_DEFAULT
def _hover_resize_edge(self) -> int:
hw: int = self.hit_test(self.cx, self.cy)
if hw < 0: return 0
return self._resize_edge_at(self.slot(hw), self.cx, self.cy)
# 调整窗口外框几何。像素缓冲只在容量不足时扩容:arena 是 bump 分配,
# 容量内复用可避免反复最大化/还原把 arena 耗尽。返回 1 成功 / 0 失败。
def _set_window_rect(self, s: desk.WinSlot*, x: int, y: int, w: int, h: int) -> int:
@@ -324,13 +512,18 @@ class DeskServer:
need: int = w * ch
if w <= 0 or need <= 0: return 0
if need > int(s.fb_cap):
off: uint32 = desk.desk_arena_alloc(self.shm, uint32(need * 4))
# 交互缩放时按 1.5 倍增长,避免拖拽过程中每加几像素就切一块新 arena;
# 最大化的一次性大跨度仍会直接按 need 分配。
alloc_px: int = need
grow: int = int(s.fb_cap) + int(s.fb_cap) / 2
if grow > alloc_px: alloc_px = grow
off: uint32 = desk.desk_arena_alloc(self.shm, uint32(alloc_px * 4))
if off == uint32(0): return 0
s.fb_off = off
s.fb_cap = uint32(need)
s.fb_cap = uint32(alloc_px)
b: uint32* = uint32*(self.shm + uint64(off))
col: uint32 = self.rgb(uint32(30), uint32(30), uint32(50))
for k in range(need):
for k in range(alloc_px):
b[k] = col
s.ux = x
s.uy = y
@@ -340,6 +533,45 @@ class DeskServer:
s.fb_h = uint32(ch)
return 1
# 边缘拖拽的实时几何更新:只在按下期间调用,内容缓冲随尺寸同步扩容。
def _drag_resize(self):
s: desk.WinSlot* = self.slot(self.resize_win)
if s.used == 0:
self.resize_win = -1
return
dx: int = self.cx - self.resize_sx
dy: int = self.cy - self.resize_sy
nx: int = self.resize_ox
ny: int = self.resize_oy
nw: int = self.resize_ow
nh: int = self.resize_oh
if (self.resize_edge & 1) != 0:
nx = self.resize_ox + dx
nw = self.resize_ow - dx
if (self.resize_edge & 2) != 0:
nw = self.resize_ow + dx
if (self.resize_edge & 4) != 0:
ny = self.resize_oy + dy
nh = self.resize_oh - dy
if (self.resize_edge & 8) != 0:
nh = self.resize_oh + dy
if nw < MIN_WIN_W:
if (self.resize_edge & 1) != 0: nx = self.resize_ox + self.resize_ow - MIN_WIN_W
nw = MIN_WIN_W
if nh < MIN_WIN_H:
if (self.resize_edge & 4) != 0: ny = self.resize_oy + self.resize_oh - MIN_WIN_H
nh = MIN_WIN_H
if nx < 0: nx = 0
if ny < 0: ny = 0
if nx + nw > self.fb_w: nw = self.fb_w - nx
if ny + nh > self.fb_h: nh = self.fb_h - ny
if nx == s.ux and ny == s.uy and nw == s.w and nh == s.h: return
self.mark_win(s.uy, s.h)
if self._set_window_rect(s, nx, ny, nw, nh) != 0:
self.push_event(self.resize_win, uint32(desk.DESK_EVT_RESIZE),
int(s.fb_w), int(s.fb_h), 0, 0)
self.mark_win(s.uy, s.h)
def _focus_top_visible(self):
best: int = -1
bz: int = -1
@@ -362,6 +594,7 @@ class DeskServer:
if self.focused == win: self.focused = -1
if self.drag_win == win: self.drag_win = -1
if self.capture_win == win: self.capture_win = -1
if self.resize_win == win: self.resize_win = -1
if self.btn_win == win:
self.btn_win = -1
self.btn_kind = 0
@@ -377,6 +610,7 @@ class DeskServer:
if self.focused == win: self.focused = -1
if self.drag_win == win: self.drag_win = -1
if self.capture_win == win: self.capture_win = -1
if self.resize_win == win: self.resize_win = -1
self._focus_top_visible()
self.mark_all_windows()
self.mark_titles()
@@ -823,7 +1057,7 @@ class DeskServer:
if hi > lo:
self.row_fill(bx + lo, yy, hi - lo, col)
def _draw_close_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, pressed: int):
def _draw_close_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, hovered: int, pressed: int):
lry: int = yy - by
if lry < 0 or lry >= bh: return
cx: int = bx + bw / 2
@@ -831,50 +1065,73 @@ class DeskServer:
d: int = 3
if bw >= 30: d = 4
x_col: uint32 = self.rgb(uint32(200), uint32(200), uint32(200))
active: int = 1 if (hovered != 0 or pressed != 0) else 0
if style == STYLE_CLASSIC:
base_col: uint32 = self.rgb(uint32(40), uint32(40), uint32(70))
if pressed != 0: base_col = self.rgb(uint32(20), uint32(20), uint32(45))
if active != 0: base_col = self.rgb(uint32(200), uint32(50), uint32(50))
if pressed != 0: base_col = self.rgb(uint32(140), uint32(25), uint32(25))
self.row_fill(bx, yy, bw, base_col)
if active != 0: x_col = self.rgb(uint32(255), uint32(255), uint32(255))
elif style == STYLE_WIN7 or style == STYLE_WINXP:
base_col2: uint32 = self.rgb(uint32(200), uint32(50), uint32(50))
if hovered != 0: base_col2 = self.rgb(uint32(220), uint32(60), uint32(60))
if pressed != 0: base_col2 = self.rgb(uint32(140), uint32(25), uint32(25))
self._round_fill_row(bx, by, bw, bh, yy, base_col2, 3)
x_col = self.rgb(uint32(255), uint32(255), uint32(255))
else:
x_col = self.rgb(uint32(150), uint32(150), uint32(150))
# WIN10/WIN11 平时无底色,按下时补一块深灰底做按压反馈
if pressed != 0:
self.row_fill(bx, yy, bw, self.rgb(uint32(70), uint32(70), uint32(70)))
# WIN10/WIN11 平时无底色;hover 变红、按下更深,图标保持白色
if active != 0:
bg: uint32 = self.rgb(uint32(200), uint32(50), uint32(50))
if pressed != 0: bg = self.rgb(uint32(140), uint32(25), uint32(25))
self.row_fill(bx, yy, bw, bg)
x_col = self.rgb(uint32(255), uint32(255), uint32(255))
i: int = yy - cy
if i >= 0 - d and i <= d:
self.row_fill(cx + i, yy, 1, x_col)
self.row_fill(cx - i, yy, 1, x_col)
def _draw_min_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, pressed: int):
# 方框描边的一行(x0..x0+size,size 为半宽/边宽)
def _draw_square_row(self, x0: int, y0: int, size: int, yy: int, col: uint32):
if yy < y0 or yy > y0 + size: return
if yy == y0 or yy == y0 + size:
self.row_fill(x0, yy, size + 1, col)
else:
self.row_fill(x0, yy, 1, col)
self.row_fill(x0 + size, yy, 1, col)
def _draw_min_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, hovered: int, pressed: int):
if pressed != 0:
self.row_fill(bx, yy, bw, self.rgb(uint32(70), uint32(70), uint32(70)))
self._dim_row(bx, yy, bw, 90)
elif hovered != 0:
self._dim_row(bx, yy, bw, 50)
cx: int = bx + bw / 2
cy: int = by + bh / 2
lw: int = 5
if bw >= 30: lw = 8
col: uint32 = self.rgb(uint32(180), uint32(180), uint32(180))
if hovered != 0 or pressed != 0: col = self.rgb(uint32(255), uint32(255), uint32(255))
if yy == cy + 3:
self.row_fill(cx - lw, yy, lw * 2 + 1, self.rgb(uint32(180), uint32(180), uint32(180)))
self.row_fill(cx - lw, yy, lw * 2 + 1, col)
def _draw_max_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, pressed: int):
def _draw_max_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, hovered: int, pressed: int, maximized: int):
if pressed != 0:
self.row_fill(bx, yy, bw, self.rgb(uint32(70), uint32(70), uint32(70)))
self._dim_row(bx, yy, bw, 90)
elif hovered != 0:
self._dim_row(bx, yy, bw, 50)
cx: int = bx + bw / 2
cy: int = by + bh / 2
sz: int = 4
if bw >= 30: sz = 6
dy: int = yy - cy
if dy < 0 - sz or dy > sz: return
col: uint32 = self.rgb(uint32(180), uint32(180), uint32(180))
if dy == 0 - sz or dy == 0 - sz + 1 or dy == sz:
self.row_fill(cx - sz, yy, sz * 2 + 1, col)
if hovered != 0 or pressed != 0: col = self.rgb(uint32(255), uint32(255), uint32(255))
if maximized != 0:
# 还原图标:两个错位方框(右上在后,左下在前)
self._draw_square_row(cx - 1, cy - sz, sz, yy, col)
self._draw_square_row(cx - sz, cy - 1, sz, yy, col)
else:
self.row_fill(cx - sz, yy, 1, col)
self.row_fill(cx + sz, yy, 1, col)
# 单方框;上边框只画一行(不再加厚)
self._draw_square_row(cx - sz, cy - sz, sz * 2, yy, col)
# 关闭键宽/高/右内边距(按样式;poll_input 热区复用这三者)
def _close_btn_w(self, style: int) -> int:
@@ -921,7 +1178,7 @@ class DeskServer:
if x >= min_x and x < min_x + bw: return 2
return 0
def _draw_window_btns(self, s: desk.WinSlot*, y0: int, y1: int, th: int, pressed_kind: int):
def _draw_window_btns(self, s: desk.WinSlot*, y0: int, y1: int, th: int, hover_kind: int, pressed_kind: int):
wx: int = s.ux
wy: int = s.uy
ww: int = s.w
@@ -939,9 +1196,13 @@ class DeskServer:
yend: int = y1
if yend > by + bh: yend = by + bh
while yy < yend:
self._draw_close_row(close_x, by, bw, bh, yy, style, 1 if pressed_kind == 1 else 0)
self._draw_min_row(min_x, by, bw, bh, yy, style, 1 if pressed_kind == 2 else 0)
self._draw_max_row(max_x, by, bw, bh, yy, style, 1 if pressed_kind == 3 else 0)
self._draw_close_row(close_x, by, bw, bh, yy, style,
1 if hover_kind == 1 else 0, 1 if pressed_kind == 1 else 0)
self._draw_min_row(min_x, by, bw, bh, yy, style,
1 if hover_kind == 2 else 0, 1 if pressed_kind == 2 else 0)
self._draw_max_row(max_x, by, bw, bh, yy, style,
1 if hover_kind == 3 else 0, 1 if pressed_kind == 3 else 0,
1 if s.maximized != 0 else 0)
yy += 1
# ---------------- 任务栏 ----------------
@@ -1233,7 +1494,9 @@ class DeskServer:
pressed_kind: int = 0
if self.btn_win == win: pressed_kind = self.btn_kind
self._draw_window_btns(s, y0, y1, th, pressed_kind)
hover_kind: int = 0
if self.hover_btn_win == win: hover_kind = self.hover_btn_kind
self._draw_window_btns(s, y0, y1, th, hover_kind, pressed_kind)
if s.title[0] != 0:
text_y: int = wy + (th - vpui.CHAR_H) / 2
if text_y + vpui.CHAR_H > y0 and text_y < y1:
@@ -1241,10 +1504,53 @@ class DeskServer:
if active == 0: tcol = self.rgb(uint32(170), uint32(170), uint32(170))
self.draw_str(wx + 8, text_y, &s.title[0], tcol)
# 光标带置脏:光标模板可能有 -8 的居中偏移,统一按 26 行覆盖旧/新位置
def _mark_cursor_at(self, py: int):
self.mark(py - 9, CURSOR_BMP_H + 10)
def _mark_cursor_band(self):
self._mark_cursor_at(self.cy)
def _cursor_kind(self) -> int:
# 缩放拖动期间锁定边缘光标
if self.resize_win >= 0:
return self._edge_cursor(self.resize_edge)
e: int = self._hover_resize_edge()
if e != 0:
return self._edge_cursor(e)
hw: int = self.hit_test(self.cx, self.cy)
if hw >= 0:
s: desk.WinSlot* = self.slot(hw)
if self.cy < s.uy + TITLE_H: return CURSOR_DEFAULT
c: int = s.cursor
if c < 0 or c > CURSOR_RESIZE_NESW: c = CURSOR_DEFAULT
return c
if self._taskbar_icon_hit(self.cx, self.cy) >= 0:
return CURSOR_HAND
return CURSOR_DEFAULT
def _cursor_value(self, kind: int, idx: int) -> uint8:
if kind == CURSOR_IBEAM: return _CURSOR_IBEAM[idx]
if kind == CURSOR_HAND: return _CURSOR_HAND[idx]
if kind == CURSOR_RESIZE_EW: return _CURSOR_EW[idx]
if kind == CURSOR_RESIZE_NS: return _CURSOR_NS[idx]
if kind == CURSOR_RESIZE_NWSE: return _CURSOR_NWSE[idx]
if kind == CURSOR_RESIZE_NESW: return _CURSOR_NESW[idx]
return _CURSOR_ARROW[idx]
def render_cursor_rows(self, y0: int, y1: int):
# 0 透明(不写像素,靠重画背景恢复),1 黑边,2 白心;同行连续同色合并成一次 row_fill。
cy0: int = self.cy
cy1: int = self.cy + CURSOR_BMP_H
kind: int = self._cursor_kind()
dx: int = 0
dy: int = 0
if kind == CURSOR_HAND:
dx = -7
elif kind != CURSOR_DEFAULT:
# I 形与缩放光标的热点在形状中心
dx = -8
dy = -8
cy0: int = self.cy + dy
cy1: int = cy0 + CURSOR_BMP_H
if cy1 <= y0 or cy0 >= y1: return
y: int = cy0
if y < y0: y = y0
@@ -1252,15 +1558,15 @@ class DeskServer:
row: int = (y - cy0) * CURSOR_BMP_W
col: int = 0
while col < CURSOR_BMP_W:
v: uint8 = _CURSOR_ARROW[row + col]
v: uint8 = self._cursor_value(kind, row + col)
if v == 0:
col += 1
continue
start: int = col
col += 1
while col < CURSOR_BMP_W and _CURSOR_ARROW[row + col] == v:
while col < CURSOR_BMP_W and self._cursor_value(kind, row + col) == v:
col += 1
px: int = self.cx + start
px: int = self.cx + dx + start
seg_w: int = col - start
if px + seg_w > 0 and px < self.fb_w:
if px < 0:
@@ -1371,6 +1677,12 @@ class DeskServer:
elif ty == uint32(desk.DESK_REQ_TITLE):
string.memcpy(&s.title, &m.title, uint64(desk.DESK_TITLE_MAX))
self.mark(s.uy, TITLE_H)
elif ty == uint32(desk.DESK_REQ_CURSOR):
if s.cursor != m.a:
s.cursor = m.a
# 光标正压在这个窗口上:立即按新形状重画光标带
if self.hit_test(self.cx, self.cy) == win:
self._mark_cursor_band()
elif ty == uint32(desk.DESK_REQ_FLUSH):
# 【临时诊断】首条 FLUSH:证明 app 的"请合成"真的到达了合成器
if _flush_cnt == 0:
@@ -1509,8 +1821,8 @@ class DeskServer:
cur = bi
mn = fs.read(self.mfd, &mp, uint32(16))
if moved != 0:
self.mark(self.pcy - 1, CURSOR_BMP_H + 2)
self.mark(self.cy - 1, CURSOR_BMP_H + 2)
self._mark_cursor_at(self.pcy)
self._mark_cursor_at(self.cy)
self.pcx = self.cx
self.pcy = self.cy
# 按住标题栏按钮时移出热区:撤销按下态并置脏恢复
@@ -1550,9 +1862,20 @@ class DeskServer:
hit = self.hit_test(self.cx, self.cy)
if hit >= 0:
s: desk.WinSlot* = self.slot(hit)
edge: int = self._resize_edge_at(s, self.cx, self.cy)
kind: int = self._title_btn_hit(s, self.cx, self.cy)
self.focus(hit)
if kind != 0:
if edge != 0:
# 窗口边缘 ⇒ 拖拽缩放
self.resize_win = hit
self.resize_edge = edge
self.resize_ox = s.ux
self.resize_oy = s.uy
self.resize_ow = s.w
self.resize_oh = s.h
self.resize_sx = self.cx
self.resize_sy = self.cy
elif kind != 0:
# 标题栏按钮:只进入按下态,松手且仍在同一按钮上才执行
self.btn_win = hit
self.btn_kind = kind
@@ -1592,6 +1915,15 @@ class DeskServer:
self._minimize_window(bw)
elif bk == 3:
self._toggle_maximize(bw)
elif self.resize_win >= 0:
# 缩放结束:补发一次最终 RESIZE,防止拖动期间事件环满导致 app 停在旧尺寸。
rs: desk.WinSlot* = self.slot(self.resize_win)
if rs.used != 0:
self.push_event(self.resize_win, uint32(desk.DESK_EVT_RESIZE),
int(rs.fb_w), int(rs.fb_h), 0, 0)
self.mark_win(rs.uy, rs.h)
self.resize_win = -1
self._mark_cursor_band()
elif self.drag_win < 0 and self.capture_win >= 0:
# 松手:投给捕获窗口(即使光标已移出窗口)
cs: desk.WinSlot* = self.slot(self.capture_win)
@@ -1601,13 +1933,39 @@ class DeskServer:
self.cx - cs.ux, self.cy - cs.uy - TITLE_H - TITLE_GAP, 0)
self.drag_win = -1
self.capture_win = -1
if self.btn_down != 0 and self.drag_win >= 0:
if self.btn_down != 0 and self.resize_win >= 0:
self._drag_resize()
elif self.btn_down != 0 and self.drag_win >= 0:
s2: desk.WinSlot* = self.slot(self.drag_win)
if s2.used != 0:
self.mark_win(s2.uy, s2.h)
s2.ux = self.cx - self.drag_dx
s2.uy = self.cy - self.drag_dy
self.mark_win(s2.uy, s2.h)
self._update_btn_hover()
# 标题栏按钮 hover 变化时只重画相关标题栏(用于变色反馈)
def _update_btn_hover(self):
nw: int = -1
nk: int = 0
if self.btn_down == 0:
hw: int = self.hit_test(self.cx, self.cy)
if hw >= 0:
s: desk.WinSlot* = self.slot(hw)
if self.cy < s.uy + TITLE_H:
k: int = self._title_btn_hit(s, self.cx, self.cy)
if k != 0:
nw = hw
nk = k
if nw == self.hover_btn_win and nk == self.hover_btn_kind: return
if self.hover_btn_win >= 0:
os: desk.WinSlot* = self.slot(self.hover_btn_win)
if os.used != 0: self.mark(os.uy, TITLE_H)
if nw >= 0:
ns: desk.WinSlot* = self.slot(nw)
self.mark(ns.uy, TITLE_H)
self.hover_btn_win = nw
self.hover_btn_kind = nk
# ---------------- 主循环 ----------------
def serve(self):
+1
View File
@@ -53,6 +53,7 @@ define DESK_REQ_RESIZE = 4 # a=w b=h
define DESK_REQ_TITLE = 5 # title 在数据区
define DESK_REQ_FLUSH = 6 # 像素已写,请求合成该窗口
define DESK_REQ_ACTIVATE = 7 # 置顶并聚焦
define DESK_REQ_CURSOR = 8 # a=cursor type(app 请求窗口内光标形状)
# 消息类型:事件(explorer → app)
define DESK_EVT_CREATED = 101 # a=win_id(失败为 -1)
define DESK_EVT_CLOSE = 102 # 用户点了关闭按钮,app 应当退出
+24 -6
View File
@@ -828,20 +828,14 @@ virtual class Entry(Widget):
if self.contains(ex, ey):
if not self.hovered:
self.hovered = 1
tk: Tk* = self.tk
window.set_cursor_type(tk.win_id, window.CURSOR_IBEAM)
return 1
else:
if self.hovered:
self.hovered = 0
tk2: Tk* = self.tk
window.set_cursor_type(tk2.win_id, window.CURSOR_DEFAULT)
return 1
elif etype == window.EVENT_TYPE_HOVER_LEAVE:
if self.hovered:
self.hovered = 0
tk3: Tk* = self.tk
window.set_cursor_type(tk3.win_id, window.CURSOR_DEFAULT)
return 1
elif etype == window.EVENT_TYPE_CLICK:
if self.contains(ex, ey):
@@ -1550,6 +1544,7 @@ virtual class Tk(Wm, Misc):
widgets: [MAX_WIDGETS]Widget*
widget_count: int
focused_widget: Widget*
hovered_widget: Widget*
_win_x: int
_win_y: int
_title: [32]char
@@ -1569,6 +1564,7 @@ virtual class Tk(Wm, Misc):
self.tk = Tk*(self)
self.widget_count = 0
self.focused_widget = None
self.hovered_widget = None
self.win_id = -1
self._win_x = 0
self._win_y = 0
@@ -1923,6 +1919,27 @@ virtual class Tk(Wm, Misc):
evt.x = vk
return 1
# 指针下的最上层控件决定光标形状:Entry/Text → I 形,Button → 手指。
# 与控件自己的 hover 视觉解耦(后者仍由 dispatch_event 维护)。
def _update_cursor_for_hover(self, ex: int, ey: int):
hw: Widget* = None
i: int = self.widget_count - 1
while i >= 0:
w: Widget* = self.widgets[i]
if w.visible and w.contains(ex, ey):
hw = w
break
i -= 1
if hw == self.hovered_widget: return
self.hovered_widget = hw
ct: int = window.CURSOR_DEFAULT
if hw is not None:
if hw.widget_type == WIDGET_ENTRY or hw.widget_type == WIDGET_TEXT:
ct = window.CURSOR_IBEAM
elif hw.widget_type == WIDGET_BUTTON:
ct = window.CURSOR_HAND
window.set_cursor_type(self.win_id, ct)
def mainloop(self):
self.redraw()
evt: window.InputEvent
@@ -1956,6 +1973,7 @@ virtual class Tk(Wm, Misc):
if handled: need_redraw = 1
else:
if etype == window.EVENT_TYPE_HOVER:
self._update_cursor_for_hover(evt.x, evt.y)
i: int = self.widget_count - 1
while i >= 0:
w: Widget* = self.widgets[i]
+6 -2
View File
@@ -30,6 +30,8 @@ define CURSOR_IBEAM = 1
define CURSOR_RESIZE_EW = 2
define CURSOR_RESIZE_NS = 3
define CURSOR_RESIZE_NWSE = 4
define CURSOR_HAND = 5
define CURSOR_RESIZE_NESW = 6
define VK_LEFT = -1
define VK_RIGHT = -2
@@ -241,8 +243,10 @@ def poll_event(evt: InputEvent*) -> int:
return 0
def set_cursor_type(win_id: int, cursor_t: int):
# TODO(S3c-2):光标形状目前由 explorer 统一画成方框,尚无按窗口切换的请求。
pass
"""请求窗口内光标形状(输入框 I 形、按钮手指、窗口边缘缩放等)。"""
if _connect() != 0: return
if win_id < 0 or win_id >= desk.DESK_MAX_WIN: return
_send(uint32(desk.DESK_REQ_CURSOR), win_id, cursor_t, 0, 0, 0, None)
def set_title(win_id: int, title: const str) -> int:
return _send(uint32(desk.DESK_REQ_TITLE), win_id, 0, 0, 0, 0, title)