From b9efc30d7134dcbeefe8313701f045169de6d49f Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 27 Oct 2026 13:14:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(window):=20=E6=A0=87=E9=A2=98=E6=A0=8F=20h?= =?UTF-8?q?over=E3=80=81=E6=9C=80=E5=A4=A7=E5=8C=96=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E3=80=81=E8=BE=B9=E7=BC=98=E7=BC=A9=E6=94=BE=E4=B8=8E=E6=8E=A7?= =?UTF-8?q?=E4=BB=B6=E5=85=89=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 自己的光标请求移除,避免与集中逻辑冲突。 --- Apps/Explorer/desk_server.vp | 424 ++++++++++++++++++++++++++++++++--- vpsdk/desk.vp | 1 + vpsdk/vpui.vp | 30 ++- vpsdk/window.vp | 8 +- 4 files changed, 422 insertions(+), 41 deletions(-) diff --git a/Apps/Explorer/desk_server.vp b/Apps/Explorer/desk_server.vp index 7b676a2..5546072 100644 --- a/Apps/Explorer/desk_server.vp +++ b/Apps/Explorer/desk_server.vp @@ -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): diff --git a/vpsdk/desk.vp b/vpsdk/desk.vp index f9f0e74..c69e1df 100644 --- a/vpsdk/desk.vp +++ b/vpsdk/desk.vp @@ -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 应当退出 diff --git a/vpsdk/vpui.vp b/vpsdk/vpui.vp index 7d2f605..c60ac2d 100644 --- a/vpsdk/vpui.vp +++ b/vpsdk/vpui.vp @@ -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] diff --git a/vpsdk/window.vp b/vpsdk/window.vp index bc766cd..057acff 100644 --- a/vpsdk/window.vp +++ b/vpsdk/window.vp @@ -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)