Skip to content

导引:点亮什么、为什么、设计图

到 029 为止,内核有了一块「画布」——能在屏幕上画矩形、画字、flip() 刷新一帧。但那张画是死的:开机画一次,之后就再也不变。你动鼠标没反应,敲键盘没反应,更别提什么窗口。这一章,我们把这块静态画布变成一个会动的桌面:接上第一个指针设备(PS/2 鼠标),搭起一套统一的事件管线,做出能拖动、能关闭、有 Z 序的窗口,再让一个窗口管理器每帧把所有窗口合成到屏幕上。做完这一步,内核第一次有了「图形交互」——你点哪儿,它知道。

这一章我们要点亮什么

一件最直观的事:GUI 模式开机后,屏幕上出现三个错落的窗口;你拖动标题栏,窗口跟着鼠标走;点窗口把它顶到最前面;点右上角红叉把它关掉。整个链路是:

text
你动鼠标 → PS/2 鼠标发 IRQ12 → 我们的 handler 读 3 字节包、解出 dx/dy/按键
        → 入一个统一事件队列 EventQueue
PIT 每个滴答 → 把队列里的事件排空 → 交给窗口管理器
        → 窗口管理器算命中、改窗口位置 → 把所有窗口按 Z 序重新合成到屏幕 → flip()

这件「拖窗口」的小事背后,点亮了好几个全新的东西:

  • 第一个指针设备。前面 014 接过键盘(IRQ1),这一章接鼠标(IRQ12)。和键盘一样是 PS/2,但鼠标有个键盘没有的麻烦:它只报告相对位移,你得自己累积成绝对坐标。
  • 第一套统一事件系统。鼠标事件、键盘事件被收进同一个 EventQueue,由一个排空点统一消费。这是 GUI 内核里反复出现的「生产/消费」分离,和 014 的键盘 ring buffer 是同一思路的升级版。
  • 第一个窗口管理器。Z 序、合成、命中测试、拖拽——一个窗口管理器该有的骨架,这一章全搭起来。
  • 第一次被逼着正视 System V ABI 栈对齐。鼠标初始化时碰了 PS/2 控制器,触发了一个一直潜伏的栈对齐 bug,一开机就 #GP。这条调试线是这一章最有含金量的部分。

为什么现在需要它

回顾 029 给我们留下的东西:canvas.hpp 里的 Canvas 类,它有一个离屏的 back buffer、一个指向 framebuffer 的 front buffer,会 draw_pixel / draw_rect / draw_text,会 blit(把一块画布拷到另一块上)、会 flip()(把 back buffer 拷到屏幕)。gui_init() 用它画了 10 个随机色矩形 + 一句 Cinux GUI,然后 flip() 一下,完事。

问题很明显:

第一,没有输入。这块画布画完就不动了,内核对外面发生什么一无所知。要交互,得先有「能报告坐标和按键」的东西。

第二,没有「窗口」这个抽象。029 的画面是直接往整块屏幕画。可真正的桌面是「一块块独立的矩形区域叠在一起」,每个区域有自己的内容、能被拖动、能被点中、谁压在谁上面有讲究。没有窗口抽象,就没法谈交互——你连「点到的是哪个东西」都说不清。

所以 030 要补两层:底下是输入管线(鼠标驱动 + 事件队列),上面是窗口模型(Window + WindowManager)。两层合起来,画布才活起来。

030 还顺手给 Canvas 加了一个新构造函数 init(uint32_t w, uint32_t h),造一块不挂 framebuffer 的纯离屏画布:

cpp
void Canvas::init(uint32_t w, uint32_t h) {
    front_buf_ = nullptr;          // 没有 front buffer
    width_ = w; height_ = h;
    pitch_ = w * 4;                // 4 字节/像素,无对齐填充
    back_buf_ = new uint32_t[w * h];
    memfill32(back_buf_, 0, w * h);
}

为什么需要它?因为窗口要双缓冲:每个窗口先画到自己的离屏画布上(标题栏 + 内容),合成时再整块 blit 到屏幕。离屏画布的 flip() 是 no-op(没有 front buffer),完全合理。这个小改动是后面整个窗口模型的基石。

设计图

整个 030 的交互管线长这样:

展开代码 (共 28 行)收起代码
text
┌─────────────┐  IRQ1   ┌──────────────────────────┐
│  键盘 PS/2   │────────▶│ Keyboard::irq1_handler    │──┐
└─────────────┘         │  (双路分发:原队列 + GUI)  │  │
                        └──────────────────────────┘  │

┌─────────────┐  IRQ12  ┌──────────────────────────┐  enqueue
│  鼠标 PS/2   │────────▶│ Mouse::irq12_handler      │──────────┐
└─────────────┘         │  (3 字节包 → MouseEvent)   │          │
                        └──────────────────────────┘          │

                                                    ┌─────────────────────┐
                                                    │   EventQueue (128)   │  ← 统一事件队列
                                                    │   head_ / tail_      │
                                                    └──────────┬──────────┘
                                                               │ dequeue
                        ┌──────────────────────────┐          │
PIT IRQ0 ──────────────▶│ gui_tick_callback         │──────────┘
                        │  排空队列 → handle_mouse   │
                        │            → composite()  │
                        └─────────────┬────────────┘

                        ┌──────────────────────────┐
                        │  WindowManager            │
                        │  windows_[64] (Z 序)      │
                        │  命中 / 拖拽 / raise      │
                        └─────────────┬────────────┘

                        clear(桌面色) → 按 Z 序 blit 各窗口 → 画光标 → flip()

窗口在 Z 序里的叠放,和合成时的绘制顺序,是理解「为什么点的是这个窗口」的关键:

text
windows_[] 数组:  index 0 (最底)  ............  index count-1 (最顶)
合成顺序:        先画 index 0 ──────────────────▶ 最后画 index count-1
命中测试顺序:    从 index count-1 ──────────────▶ 往下到 index 0  (顶上的先被命中)

                  ┌──────────────┐
   index 2 (顶)   │   Window 3    │  ← 先画,但命中时最先检查
                  ├──────┬───────┘
   index 1        │ Win2 │
                  ├──┬───┘
   index 0 (底)   │W1│   ← 最后检查的命中候选
                  └──┘

数组的下标既表示 Z 序(0 最底),也直接决定了「画的时候从底往顶、点的时候从顶往底」这两条相反的扫描方向。这个对称性是窗口管理器最核心的设计。

035_multi_terminal-45-gf25de18 · f25de18 · 2026-08-04