# 单色点阵屏GUI **Repository Path**: SiaZhang/DotMatrixGUI ## Basic Information - **Project Name**: 单色点阵屏GUI - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-08 - **Last Updated**: 2026-05-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # GUI 项目 — 裁剪(Clipping)机制说明 ## 概要 - 本仓库在 `UI` 层实现了“层级矩形裁剪(hierarchical rectangular clipping)”机制,保证父容器限制其子组件的可视区域,超出父级范围的像素不会被绘制。 ## 关键思路(一行总结) 父容器先与当前裁剪区取交集,得到新的裁剪区并传给子组件;子组件再与该裁剪区求交集,最终只在交集区域内真正绘制。 ## 核心实现位置 - 通用矩形与裁剪工具:[inc/ui_common.h](inc/ui_common.h) - 全局 UI 状态(含当前裁剪区):[inc/ui.h](inc/ui.h) - UI 初始化(裁剪区默认设为整屏):[src/ui.c](src/ui.c) - 列表组件(按父级裁剪再传给文本项):[src/ui_list.c](src/ui_list.c) - 文本组件(与当前裁剪区求交集后绘制):[src/ui_text.c](src/ui_text.c) - 画布/画笔封装(push/pop canvas + 裁剪绘制):[inc/ui_canvas.h](inc/ui_canvas.h) 与 [src/ui_canvas.c](src/ui_canvas.c) ## 详细流程(建议照此实现新组件) 建议改为“画布/画笔”抽象: 1. 组件在绘制前,构造自身边界矩形 `rect = UI_RectMake(x, y, width, height)`。 2. 使用画布 API 推入画布:`UI_PushCanvas(&g_ui, &rect, &canvas_state)`。该调用会把 `g_ui.clip_rect` 与 `rect` 求交集,得到新的有效画布并保存父级状态到 `canvas_state`。 - 如果 `UI_RectIsEmpty(&canvas_state.rect)`,说明画布不可见,调用 `UI_PopCanvas` 并返回。 3. 使用画笔/绘制助手在当前画布上绘制,例如: - `UI_DrawRectClipped(&g_ui, &rect, color)` — 绘制矩形(自动裁剪至当前画布); - `UI_DrawPixelClipped(&g_ui, x, y, color)` — 绘制单像素(自动检验是否在画布内); - 将来可添加 `UI_DrawTextClipped`、`UI_DrawImageClipped` 等函数,所有函数都以当前 `g_ui.clip_rect` 为最终约束。 4. 如果有子组件,直接在当前画布上下文中调用它们(它们可以继续 `UI_PushCanvas` 更细粒度裁剪)。 5. 绘制完成后恢复父级画布:`UI_PopCanvas(&g_ui, &canvas_state)`。 ## 注意与建议 - `clip_rect` 存放在全局 `g_ui.clip_rect`(见 `UI_Obj_t`),初始化为整屏像素矩形(见 `UI_Init`)。 - 当前实现把“边框绘制”与“drawable 区域裁剪”结合起来:即边框也会被裁剪。 - 目前 `UI_Text` 的代码只绘制了文本项的边框(示例),并未实现逐字符/逐像素的字形渲染;当你实现字形绘制时,需要在绘制每个像素前判断是否在 `ui->clip_rect` 内,或按字符宽度计算可绘区域后只绘制可见部分。 - 推荐的组件实现规范: - 组件应只关心自身边界与内容布局。 - 所有绘制入口(含边框、背景、内容)都应以 `ui->clip_rect` 为最终约束。 - 对于复杂项(如滚动视图、长文本、多列布局),先计算可视子区域再遍历渲染项,避免无谓像素绘制。 ## 示例(伪代码) ``` UI_Rect_t rect = UI_RectMake(x, y, w, h); UI_Canvas_t canvas_state; UI_PushCanvas(&g_ui, &rect, &canvas_state); if (UI_RectIsEmpty(&canvas_state.rect)) { UI_PopCanvas(&g_ui, &canvas_state); return; } UI_DrawRectClipped(&g_ui, &rect, color); // draw children or text; use UI_DrawPixelClipped for per-pixel safety UI_PopCanvas(&g_ui, &canvas_state); ``` ## 变更记录 - 初次添加(2026-05-09):实现 `UI_Rect` 工具、在 `UI_Obj_t` 中存储 `clip_rect`,并在 `src/ui_list.c` 与 `src/ui_text.c` 中按层级裁剪实现组件限制。 ## 后续任务(可选) - 在 `UI_Text` 中补充实际的字形渲染(位图或字体库),并在渲染过程中保证按 `g_ui.clip_rect` 做像素级裁剪。 - 为底层绘图接口增加“scissor/clip”原语(如果底层设备支持),这样可以把裁剪交给驱动以提升性能。 --- 如果你要我,我可以: - 把 `UI_Text` 的字形绘制补上(逐字符或逐像素裁剪); - 或者把裁剪封装成一组 API(`UI_PushClip`/`UI_PopClip`)以便复用。 ## 新增文件规划(建议) - `inc/ui_canvas.h` / `src/ui_canvas.c` — 画布/画笔抽象、`UI_PushCanvas` / `UI_PopCanvas`、以及 `UI_Draw*Clipped` 辅助函数。 - `inc/ui_font.h` / `src/ui_font.c` — 字形位图与 `UI_DrawTextClipped`(将使用 `UI_DrawPixelClipped` 来保证裁剪)。 - `src/ui_draw_helpers.c`(可选)— 将与画布相关的辅助绘制函数集中放置,避免组件间重复实现。 ## 计划(待办) 下面为未来可能的功能改进计划,暂作记录: - Add mode state to UI (NAV/EDIT/MODAL) — 在页面层维护轻量 `mode`,控制输入路由。 - Define component edit interface (enter_edit/exit_edit/on_key) — 组件提供编辑入口与键处理接口。 - Implement in-place list item editor (buffer + draw) — 列表项支持原地编辑(临时缓冲 + 光标渲染)。 - Implement modal editor option — 对于复杂编辑使用模态编辑器。 - Integrate event router / state machine into UI_Events — 在事件循环中按 `mode` 转发按键。 - Define confirm/cancel callback flow to page/model — 编辑提交/取消走回调并更新模型。 - Write example event flow walkthrough — 为实现和测试写具体时序图或示例。 (注:以上为计划记录,后续可据此分步实现) ## 交互编辑方案(记录) 下面是基于「组件内处理 + 输入路由(状态机) + 可选模态」的交互编辑方案摘要,作为后续实现参考: - 概述:结合组件自身封装与页面/全局的输入路由机制,通过简洁的 `mode`(`NAV` / `EDIT` / `MODAL`)控制按键分发。 - 模式(page-level `mode`): - `NAV`:默认导航模式,按键用于切换焦点(页面处理导航逻辑)。 - `EDIT`:组件进入编辑态时的模式,所有按键优先由该组件处理。 - `MODAL`:弹出式编辑器或复杂编辑时的模式,输入路由到模态编辑器。 - 组件编辑接口(建议约定): - `enter_edit(component)`:组件进入编辑态,初始化编辑缓冲并切 `mode = EDIT`。 - `exit_edit(component, save)`:组件退出编辑态,`save` 表示确认/取消;退出时恢复 `mode = NAV`(或由页面决定)。 - `on_key(component, key)`:当 `mode == EDIT` 时接收按键,由组件处理并返回是否已消费。 - 回调:`on_edit_commit(component, new_value)`、`on_edit_cancel(component)`,由组件在确认/取消时触发页面或控制器处理。 - 编辑实现建议: - 简单 `name [value]` 场景:优先使用 in-place 编辑(列表项维护 `edit_buffer` + `draw()` 显示光标)。 - 复杂字段(多行、表单、选择器):使用模态编辑器(浮层或子页面),编辑完成回调页面。 - 对于列表项,可把项设计为持有 `component` 指针与 `draw` 回调,进入编辑态后该项切换为编辑渲染。 - 事件路由(建议实现): - 在 `UI_Events` 或输入循环:读取按键 `key`。 - 若 `mode == EDIT`:转发到 `focused_component->on_key(key)`;组件返回已消费则停止进一步处理。 - 若 `mode == MODAL`:转发到模态编辑器处理。 - 若 `mode == NAV`:按现有导航逻辑处理(Up/Down 切焦点、Enter 触发 `enter_edit` 或页面行为)。 - 示例流程(典型交互): 1. 页面 `mode = NAV`,焦点在列表项 `Brightness [50]`。 2. 用户按 `Enter`(NAV):页面检测组件可编辑,调用 `enter_edit()`;切 `mode = EDIT`。 3. 组件在 `enter_edit` 中保存 `original_value`,初始化 `edit_buffer`,并在 `draw()` 中呈现光标。 4. 编辑期间(`mode = EDIT`):按键全部由组件的 `on_key()` 处理(如 ↑/↓ 改值,Esc 取消,Enter 确认)。 5. 组件 `exit_edit(save=true)` 后触发 `on_edit_commit` 回调,页面接收并更新数据模型或触发持久化,随后 `mode = NAV`。 6. 若编辑超出 in-place 能力,`enter_edit` 可触发 modal(页面切 `mode = MODAL` 并显示编辑对话)。 - 推荐组合与理由: - 使用 **组件内处理 + 输入路由(状态机) + 可选模态** 的组合:既把组件的细节封装到组件内部(复用性好),又由页面/全局负责模式切换和高层流程(验证、持久化),利于扩展与测试。 - UX 要点(简短): - 明确视觉反馈:编辑态需明显高亮或显示光标,模态要有遮罩。 - 键位约定:`Enter` 确认、`Esc` 取消、`Up/Down` 在 NAV 为导航、在 EDIT 为调整值/移动光标。 - 持久化策略:决定是即时写模型还是确认后写(可根据需求决定撤销/事务策略)。 以上为讨论记录,后续可据此细化状态机图与接口签名并逐项实现。 ## 对话总结(细化记录) 下面把我们围绕 `page`、`component`、`list item`、编辑态和事件路由的结论做一次更细化的落盘,方便后续直接照着实现: ### 1. 核心分层 - `page` 更适合作为父上下文和调度中心,不建议继承 `base`。 - `component` 才是实际 UI 单元,持有 `UI_CompBase_t`,负责绘制与局部交互。 - `list item` 是更细一级的行为对象,不一定都对应同一种组件,可以混放参数项、功能项、复杂项。 ### 2. page 是否继承 `base` - 当前结论是不需要。 - 原因是 `page` 的职责是模式管理、焦点管理、事件路由、页面切换,而不是作为普通可绘制组件参与统一继承。 - 如果未来需要把 `page` 也纳入统一绘制或布局体系,可以再考虑单独引入轻量 `page_base`,但不建议和组件体系强行混合。 ### 3. 编辑态怎么知道 - 不是组件自己“猜”页面状态,而是 `page` 在进入编辑时明确通知当前组件。 - `page` 维护当前 `mode`,同时记录当前正在编辑的对象。 - 进入 `EDIT` 后,按键只发给当前编辑对象;退出编辑后再回到 `NAV`。 ### 4. page 作为父上下文 - 可以,把它当成父组件的上下文来理解是合理的。 - 但更准确地说,它是“上下文 + 调度器”,而不是普通组件。 - 推荐它只保留轻量上下文字段,例如:`mode`、`focused_component`、`active_editor`、`request_refresh`、确认/取消回调等。 ### 5. list 里面既有参数项也有功能项 - 这是合理的,而且建议这么做。 - 参数项:按 Enter 进入编辑态,例如 `Brightness [50]`、`Volume [80]`。 - 功能项:按 Enter 直接触发动作,例如 `Reset WiFi`、`Scan Devices`。 - 所以 `list` 只是容器,真正的语义由 item 自己决定。 ### 6. Enter 时怎么决定“编辑还是执行” - 不应该由页面硬编码一堆 `switch` 来猜。 - 更好的做法是:item 自己带行为回调,list 只负责把 Enter 转发给当前项。 - 当前项的回调返回结果可以是:进入编辑、直接执行、打开模态、忽略。 ### 7. 参数项与功能项的统一规则 - 每个 item 建议携带:类型标记、绘制回调、Enter 回调、可选键处理回调、可选提交/取消回调。 - 页面只做统一路由:在 `NAV` 下移动焦点,在 `Enter` 时调用当前项的行为。 - 这样就能在同一个 list 里混放“参数”和“功能”,而不用页面去分辨所有细节。 ### 8. 最终推荐结构 - `page`:父上下文,负责 `mode`、焦点、事件分发、编辑上下文。 - `component`:负责自己的绘制、编辑和交互。 - `list item`:负责自己在 Enter 之后是“编辑”还是“执行”。 - `mode`:用 `NAV / EDIT / MODAL` 控制输入路由。 ### 9. 这套方案的优先级 - 最推荐的方案是:`page` 做上下文,`component` 做行为,`mode` 做路由,`list item` 做语义决定。 - 不推荐把所有逻辑都堆在 `page` 里,也不推荐让组件自己去猜页面状态。 ### 10. 后续落地顺序 - 先定父上下文字段和 `mode`。 - 再定组件编辑接口。 - 再定 list item 的行为回调。 - 最后把事件路由接入 `UI_Events`。 这部分可以直接作为后续实现 `UI` 交互体系的设计备忘录。