feat(window): 鼠标捕获与窗口最小化/最大化

鼠标:
- 内容区按下后记录 capture_win,按住期间的移动仍投 HOVER、松开投 RELEASE
  (即使光标移出窗口)。此前 btn_down != 0 时完全不发 HOVER,滚动条
  拖动拿不到位置更新。
- 标题栏按钮按下态改为通用 btn_win/btn_kind,支持关闭/最小化/最大化。

窗口:
- WinSlot 新增 maximized、restore_*、fb_cap 字段
- 最小化:隐藏窗口并把焦点交给最高可见窗口;任务栏点击恢复,
  再点一次已聚焦窗口则最小化
- 最大化:保存还原几何,铺满工作区(不含任务栏)并发送 RESIZE 事件;
  再次点击还原;拖动最大化窗口会先还原
- 像素缓冲按 fb_cap 复用,仅在容量不足时从 arena 扩容,避免反复
  最大化/还原耗尽 bump arena
- 1920x1080 最大化需要约 7.3MB 内容缓冲:DESK_SHM_SIZE 8MB → 32MB
This commit is contained in:
TermiNexus committed 2026-10-27 12:43:19 +08:00
1 parent 859197e5e9
commit 632bc877df
3 files changed
+241 -78

No files matched your search

+231 -75
View File
@@ -155,10 +155,15 @@ class DeskServer:
focused: int
next_z: int
btn_down: int
close_down: int # 正在被按下的关闭键所属窗口(-1 = 无)
# 标题栏按钮的捕获态:btn_win = 窗口下标(-1 = 无),btn_kind = 0/1关闭/2最小化/3最大化
btn_win: int
btn_kind: int
drag_win: int
drag_dx: int
drag_dy: int
# 按住鼠标时的捕获窗口:内容区按下后,移动/松开事件始终投给它(滚动条等
# 控件靠按住期间的 HOVER 更新位置;移出窗口也不能丢)。
capture_win: int
# 复用缓冲
msg: desk.DeskMsg
evt: desk.DeskMsg
@@ -202,10 +207,12 @@ class DeskServer:
self.focused = -1
self.next_z = 1
self.btn_down = 0
self.close_down = -1
self.btn_win = -1
self.btn_kind = 0
self.drag_win = -1
self.drag_dx = 0
self.drag_dy = 0
self.capture_win = -1
# 预构造一行背景,渲染时整行 memcpy
self.bg_row = uint32*(syscall.mmap(0, uint64(w) * uint64(4),
uint64(syscall.PROT_READ | syscall.PROT_WRITE),
@@ -305,6 +312,120 @@ class DeskServer:
def slot(self, win: int) -> desk.WinSlot*:
return desk.WinSlot*(self.shm + desk.desk_slot_off(win))
def _content_h(self, h: int) -> int:
ch: int = h - TITLE_H - TITLE_GAP
if ch < 1: ch = 1
return ch
# 调整窗口外框几何。像素缓冲只在容量不足时扩容:arena 是 bump 分配,
# 容量内复用可避免反复最大化/还原把 arena 耗尽。返回 1 成功 / 0 失败。
def _set_window_rect(self, s: desk.WinSlot*, x: int, y: int, w: int, h: int) -> int:
ch: int = self._content_h(h)
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))
if off == uint32(0): return 0
s.fb_off = off
s.fb_cap = uint32(need)
b: uint32* = uint32*(self.shm + uint64(off))
col: uint32 = self.rgb(uint32(30), uint32(30), uint32(50))
for k in range(need):
b[k] = col
s.ux = x
s.uy = y
s.w = w
s.h = h
s.fb_w = uint32(w)
s.fb_h = uint32(ch)
return 1
def _focus_top_visible(self):
best: int = -1
bz: int = -1
for i in range(desk.DESK_MAX_WIN):
s: desk.WinSlot* = self.slot(i)
if s.used != 0 and s.active != 0 and s.minimized == 0:
if s.z_order > bz:
bz = s.z_order
best = i
self.focused = best
def _close_window(self, win: int):
s: desk.WinSlot* = self.slot(win)
if s.used == 0: return
# 关闭:通知一次 CLOSE 后把槽位置空回收;window.vp 的 is_active() 见到 used==0
# 即返回 0,app 的 while 循环自然退出。像素缓冲不回收(arena 是 bump 分配)。
self.push_event(win, uint32(desk.DESK_EVT_CLOSE), 0, 0, 0, 0)
self.mark_win(s.uy, s.h)
s.used = 0
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.btn_win == win:
self.btn_win = -1
self.btn_kind = 0
self._focus_top_visible()
self.mark_all_windows()
self.mark_titles()
def _minimize_window(self, win: int):
s: desk.WinSlot* = self.slot(win)
if s.used == 0 or s.minimized != 0: return
self.mark_win(s.uy, s.h)
s.minimized = 1
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
self._focus_top_visible()
self.mark_all_windows()
self.mark_titles()
def _maximize_window(self, win: int):
s: desk.WinSlot* = self.slot(win)
if s.used == 0 or s.maximized != 0: return
ox: int = s.ux
oy: int = s.uy
ow: int = s.w
oh: int = s.h
mw: int = self.fb_w
mh: int = self.fb_h - TASKBAR_H
if mh < TITLE_H + TITLE_GAP + 1: return
if self._set_window_rect(s, 0, 0, mw, mh) == 0: return
s.restore_x = ox
s.restore_y = oy
s.restore_w = ow
s.restore_h = oh
s.maximized = 1
self.mark_win(oy, oh)
self.mark_win(s.uy, s.h)
self.push_event(win, uint32(desk.DESK_EVT_RESIZE), int(s.fb_w), int(s.fb_h), 0, 0)
self.mark_all_windows()
self.mark_titles()
def _restore_window(self, win: int) -> int:
s: desk.WinSlot* = self.slot(win)
if s.used == 0 or s.maximized == 0: return 0
oy: int = s.uy
oh: int = s.h
if self._set_window_rect(s, s.restore_x, s.restore_y, s.restore_w, s.restore_h) == 0:
return 0
s.maximized = 0
self.mark_win(oy, oh)
self.mark_win(s.uy, s.h)
self.push_event(win, uint32(desk.DESK_EVT_RESIZE), int(s.fb_w), int(s.fb_h), 0, 0)
self.mark_all_windows()
self.mark_titles()
return 1
def _toggle_maximize(self, win: int):
s: desk.WinSlot* = self.slot(win)
if s.used == 0: return
if s.maximized != 0:
self._restore_window(win)
else:
self._maximize_window(win)
# 最高 z 序(用于判定活动窗口;无可见窗口返回 -1)
def _top_z(self) -> int:
m: int = -1
@@ -338,26 +459,33 @@ class DeskServer:
for i in range(desk.DESK_MAX_WIN):
s: desk.WinSlot* = self.slot(i)
if s.used == 0:
buf_off: uint32 = desk.desk_arena_alloc(self.shm, uint32(w) * uint32(h) * uint32(4))
cap_px: int = w * content_h
buf_off: uint32 = desk.desk_arena_alloc(self.shm, uint32(cap_px) * uint32(4))
if buf_off == uint32(0): return -1
s.used = 1
s.pid = pid
s.active = 1
s.minimized = 0
s.maximized = 0
s.ux = x
s.uy = y
s.w = w
s.h = h
s.restore_x = x
s.restore_y = y
s.restore_w = w
s.restore_h = h
s.style = style
s.cursor = 0
s.fb_off = buf_off
s.fb_cap = uint32(cap_px)
s.fb_w = uint32(w)
s.fb_h = uint32(content_h)
string.memset(&s.title, 0, desk.DESK_TITLE_MAX)
# 像素缓冲先清成窗口底色,避免 app 还没画就露出垃圾
buf: uint32* = uint32*(self.shm + uint64(buf_off))
col: uint32 = self.rgb(uint32(30), uint32(30), uint32(50))
for k in range(w * h):
for k in range(cap_px):
buf[k] = col
s.z_order = self.next_z
self.next_z += 1
@@ -720,7 +848,9 @@ class DeskServer:
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):
def _draw_min_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, pressed: int):
if pressed != 0:
self.row_fill(bx, yy, bw, self.rgb(uint32(70), uint32(70), uint32(70)))
cx: int = bx + bw / 2
cy: int = by + bh / 2
lw: int = 5
@@ -728,7 +858,9 @@ class DeskServer:
if yy == cy + 3:
self.row_fill(cx - lw, yy, lw * 2 + 1, self.rgb(uint32(180), uint32(180), uint32(180)))
def _draw_max_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int):
def _draw_max_row(self, bx: int, by: int, bw: int, bh: int, yy: int, style: int, pressed: int):
if pressed != 0:
self.row_fill(bx, yy, bw, self.rgb(uint32(70), uint32(70), uint32(70)))
cx: int = bx + bw / 2
cy: int = by + bh / 2
sz: int = 4
@@ -770,7 +902,24 @@ class DeskServer:
return 1
return 0
def _draw_window_btns(self, s: desk.WinSlot*, y0: int, y1: int, th: int, close_pressed: int):
# 标题栏按钮命中:1=关闭 2=最小化 3=最大化/还原(与绘制同一套几何)
def _title_btn_hit(self, s: desk.WinSlot*, x: int, y: int) -> int:
bw: int = self._close_btn_w(s.style)
bh: int = self._close_btn_h(s.style)
pad: int = self._close_btn_pad(s.style)
gap: int = 0
if s.style == STYLE_WIN7 or s.style == STYLE_WINXP: gap = 1
by: int = s.uy + (TITLE_H - bh) / 2
if y < by or y >= by + bh: return 0
close_x: int = s.ux + s.w - bw - pad
max_x: int = close_x - bw - gap
min_x: int = max_x - bw - gap
if x >= close_x and x < close_x + bw: return 1
if x >= max_x and x < max_x + bw: return 3
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):
wx: int = s.ux
wy: int = s.uy
ww: int = s.w
@@ -788,9 +937,9 @@ 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, close_pressed)
self._draw_min_row(min_x, by, bw, bh, yy, style)
self._draw_max_row(max_x, by, bw, bh, yy, style)
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)
yy += 1
# ---------------- 任务栏 ----------------
@@ -817,7 +966,7 @@ class DeskServer:
cnt += 1
return -1
# 点击图标:最小化先恢复,再走既有 focus(置顶并整体置脏)
# 点击图标:最小化先恢复;已聚焦则最小化(任务栏切换);否则聚焦
def _activate_from_taskbar(self, win: int):
if win < 0 or win >= desk.DESK_MAX_WIN: return
s: desk.WinSlot* = self.slot(win)
@@ -825,7 +974,13 @@ class DeskServer:
if s.minimized != 0:
s.minimized = 0
self.mark_win(s.uy, s.h)
self.focus(win)
self.focus(win)
self.mark_all_windows()
self.mark_titles()
elif self.focused == win:
self._minimize_window(win)
else:
self.focus(win)
def _draw_taskbar_rows(self, y0: int, y1: int):
bar_y: int = self.fb_h - TASKBAR_H
@@ -1069,9 +1224,9 @@ class DeskServer:
title_alpha, buf, fw, fh, content_y0)
yy += 1
close_pressed: int = 0
if self.close_down == win: close_pressed = 1
self._draw_window_btns(s, y0, y1, th, close_pressed)
pressed_kind: int = 0
if self.btn_win == win: pressed_kind = self.btn_kind
self._draw_window_btns(s, y0, y1, th, 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:
@@ -1194,14 +1349,8 @@ class DeskServer:
if s.used == 0: return
ty: uint32 = m.type
if ty == uint32(desk.DESK_REQ_DESTROY):
self.mark(s.uy, s.h)
s.used = 0
if self.focused == win: self.focused = -1
# 拖拽中的窗口被销毁:必须清掉 drag_win,否则后续移动会继续操作这个死槽位。
if self.drag_win == win: self.drag_win = -1
if self.close_down == win: self.close_down = -1
self.mark_all_windows()
self.mark_titles()
self._close_window(win)
return
elif ty == uint32(desk.DESK_REQ_MOVE):
self.mark_win(s.uy, s.h)
s.ux = m.a
@@ -1209,17 +1358,9 @@ class DeskServer:
self.mark_win(s.uy, s.h)
elif ty == uint32(desk.DESK_REQ_RESIZE):
self.mark_win(s.uy, s.h)
new_ch: int = m.b - TITLE_H - TITLE_GAP
if new_ch < 1: new_ch = 1
# arena 是 bump 分配、不能回收:换尺寸一律重新切片,旧块留给后续复用
new_off: uint32 = desk.desk_arena_alloc(self.shm, uint32(m.a) * uint32(m.b) * uint32(4))
if new_off != uint32(0):
s.fb_off = new_off
s.w = m.a
s.h = m.b
s.fb_w = uint32(m.a)
s.fb_h = uint32(new_ch)
self.mark_win(s.uy, s.h)
if self._set_window_rect(s, s.ux, s.uy, m.a, m.b) != 0:
s.maximized = 0
self.mark_win(s.uy, s.h)
elif ty == uint32(desk.DESK_REQ_TITLE):
string.memcpy(&s.title, &m.title, uint64(desk.DESK_TITLE_MAX))
self.mark(s.uy, TITLE_H)
@@ -1363,14 +1504,23 @@ class DeskServer:
self.mark(self.cy - 1, CURSOR_BMP_H + 2)
self.pcx = self.cx
self.pcy = self.cy
# 按住关闭键时移出按钮:撤销按下态并置脏恢复
if self.close_down >= 0:
cm: desk.WinSlot* = self.slot(self.close_down)
if cm.used == 0 or self._close_btn_hit(cm, self.cx, self.cy) == 0:
# 按住标题栏按钮时移出热区:撤销按下态并置脏恢复
if self.btn_win >= 0:
cm: desk.WinSlot* = self.slot(self.btn_win)
if cm.used == 0 or self._title_btn_hit(cm, self.cx, self.cy) != self.btn_kind:
if cm.used != 0: self.mark(cm.uy, TITLE_H)
self.close_down = -1
# 悬停:把 HOVER 投给光标下那个窗口(窗口内坐标)
if self.btn_down == 0:
self.btn_win = -1
self.btn_kind = 0
if self.btn_down != 0 and self.capture_win >= 0:
# 内容区按住期间:移动事件持续投给捕获窗口。滚动条/滑块拖动
# 依赖按住时的 HOVER;坐标允许越界,由控件自行判断。
cs: desk.WinSlot* = self.slot(self.capture_win)
if cs.used != 0:
self.push_event(self.capture_win, uint32(desk.DESK_EVT_MOUSE),
desk.DESK_MOUSE_HOVER,
self.cx - cs.ux, self.cy - cs.uy - TITLE_H - TITLE_GAP, 0)
elif self.btn_down == 0:
# 悬停:把 HOVER 投给光标下那个窗口(窗口内坐标)
hw: int = self.hit_test(self.cx, self.cy)
if hw >= 0:
hs: desk.WinSlot* = self.slot(hw)
@@ -1379,7 +1529,9 @@ class DeskServer:
self.cx - hs.ux, self.cy - hs.uy - TITLE_H - TITLE_GAP, 0)
if saw_press != 0:
self.btn_down = 1
self.close_down = -1 # 新手势开始:清掉上一次可能丢失释放沿留下的按下态
self.btn_win = -1 # 新手势开始:清掉上一次可能丢失释放沿留下的按下态
self.btn_kind = 0
self.capture_win = -1
# 任务栏图标优先:命中则激活并跳过窗口命中/拖动
hit: int = -1
tw: int = self._taskbar_icon_hit(self.cx, self.cy)
@@ -1389,20 +1541,26 @@ class DeskServer:
hit = self.hit_test(self.cx, self.cy)
if hit >= 0:
s: desk.WinSlot* = self.slot(hit)
# 关闭键热区复用 _close_btn_hit(与 _draw_window_btns 同一套几何)
in_close: int = self._close_btn_hit(s, self.cx, self.cy)
kind: int = self._title_btn_hit(s, self.cx, self.cy)
self.focus(hit)
if in_close != 0:
# 只进入按下态:松手时才真正关窗,保证按下反馈至少能刷出一帧。
self.close_down = hit
if kind != 0:
# 标题栏按钮:只进入按下态,松手且仍在同一按钮上才执行
self.btn_win = hit
self.btn_kind = kind
self.mark(s.uy, TITLE_H)
elif self.cy < s.uy + TITLE_H:
# 标题栏(非关闭按钮)⇒ 开始拖动
self.drag_win = hit
self.drag_dx = self.cx - s.ux
self.drag_dy = self.cy - s.uy
# 标题栏 ⇒ 拖动;最大化窗口先还原再拖
restored_ok: int = 1
if s.maximized != 0:
restored_ok = self._restore_window(hit)
s = self.slot(hit)
if restored_ok != 0:
self.drag_win = hit
self.drag_dx = self.cx - s.ux
self.drag_dy = self.cy - s.uy
else:
# 内容区 ⇒ 按窗口内坐标把点击投给 app
# 内容区 ⇒ 捕获鼠标:按下后的移动/松开都投给同一窗口
self.capture_win = hit
self.push_event(hit, uint32(desk.DESK_EVT_MOUSE),
desk.DESK_MOUSE_PRESS,
self.cx - s.ux, self.cy - s.uy - TITLE_H - TITLE_GAP, 1)
@@ -1410,32 +1568,30 @@ class DeskServer:
# 否则 btn_down 卡在 1,后续点击永远检测不到按下沿。
if cur == 0 and self.btn_down != 0:
self.btn_down = 0
cw_win: int = self.close_down
self.close_down = -1
if cw_win >= 0:
cs: desk.WinSlot* = self.slot(cw_win)
bw: int = self.btn_win
bk: int = self.btn_kind
self.btn_win = -1
self.btn_kind = 0
if bw >= 0:
bs: desk.WinSlot* = self.slot(bw)
if bs.used != 0:
self.mark(bs.uy, TITLE_H)
if self._title_btn_hit(bs, self.cx, self.cy) == bk:
if bk == 1:
self._close_window(bw)
elif bk == 2:
self._minimize_window(bw)
elif bk == 3:
self._toggle_maximize(bw)
elif self.drag_win < 0 and self.capture_win >= 0:
# 松手:投给捕获窗口(即使光标已移出窗口)
cs: desk.WinSlot* = self.slot(self.capture_win)
if cs.used != 0:
self.mark(cs.uy, TITLE_H)
if self._close_btn_hit(cs, self.cx, self.cy) != 0:
# 关闭:通知一次 CLOSE 后把槽位置空回收;window.vp 的 is_active()
# 见到 used==0 即返回 0,app 的 while 循环自然退出。
# 像素缓冲不回收(arena 是 bump 分配),只放槽位。
self.push_event(cw_win, uint32(desk.DESK_EVT_CLOSE), 0, 0, 0, 0)
self.mark_win(cs.uy, cs.h)
cs.used = 0
if self.focused == cw_win: self.focused = -1
if self.drag_win == cw_win: self.drag_win = -1
self.mark_all_windows()
self.mark_titles()
elif self.drag_win < 0:
# 松手:投 RELEASE 给光标下的窗口
rw: int = self.hit_test(self.cx, self.cy)
if rw >= 0:
rs: desk.WinSlot* = self.slot(rw)
self.push_event(rw, uint32(desk.DESK_EVT_MOUSE),
self.push_event(self.capture_win, uint32(desk.DESK_EVT_MOUSE),
desk.DESK_MOUSE_RELEASE,
self.cx - rs.ux, self.cy - rs.uy - TITLE_H - TITLE_GAP, 0)
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:
s2: desk.WinSlot* = self.slot(self.drag_win)
if s2.used != 0:
+1 -1
View File
@@ -575,7 +575,7 @@ def devfs_register_fb0(fb: rawptr, width: int, height: int, pitch: int, format:
# 这里用 devfs 设备 + mmap 达到同样效果)。
# ============================================================================
# 共享区大小:vpsdk/desk.vp 里有一份同名同值的镜像(用户在 mmap 时必须先知道长度)。
define DESK_SHM_SIZE = 8388608
define DESK_SHM_SIZE = 33554432
virtual class shm_device(devfs_file):
shm_phys: uint64
+9 -2
View File
@@ -26,7 +26,8 @@ from stdint import *
# 必须与内核 VKernel/Kernel/drivers/devfs/devfs.vp 的 DESK_SHM_SIZE 一致
# (mmap 时用户必须先给出长度,所以这里不能靠"问内核")。
define DESK_SHM_SIZE = 8388608
# 32MB:1920×1080 全屏窗口的内容缓冲约 7.3MB,最大化需要足够 arena。
define DESK_SHM_SIZE = 33554432
define DESK_MAGIC = 0x4B534544 # 'DESK'
define DESK_VERSION = 1
@@ -86,17 +87,23 @@ class WinSlot:
pid: int
active: int
minimized: int
maximized: int
z_order: int
ux: int
uy: int
w: int
h: int
# 最大化前的还原几何
restore_x: int
restore_y: int
restore_w: int
restore_h: int
style: int
cursor: int
fb_off: uint32
fb_w: uint32
fb_h: uint32
_pad2: uint32
fb_cap: uint32 # 像素缓冲容量(以像素计;扩容后复用,避免 arena 泄漏)
title: [DESK_TITLE_MAX]char