- 操作系统
【免费下载链接】awesome
awesome window manager
本文是 Awesome WM 官方文档 docs/04-new-widgets.md 的深度展开版。Awesome WM 的整个 UI(状态栏、标题栏、通知、托盘等)都由 widget 组成,而所有 widget 都构建在wibox.widget.base之上。读完本文,你将掌握如何从零编写一个可嵌入 wibar / popup / 通知的自定义 widget:理解:fit/:draw回调协议、widget::layout_changed与widget::redraw_needed两个核心信号、面向布局的:layout与place_widget_at,以及控制子 widget 绘制的四个钩子回调,并了解这些机制在 lib/wibox/widget/base.lua 与 lib/wibox/hierarchy.lua 中的底层实现。
一切从wibox.widget.base.make_widget开始
所有 widget 都必须由wibox.widget.base.make_widget函数生成,这是唯一的正规入口。它在 lib/wibox/widget/base.lua 中实现,负责完成三件基础工作:
- 信号体系:生成的 widget 基于
gears.object,自带信号收发能力。make_widget内部会自动连接widget::updated信号,并把它转发为widget::layout_changed与widget::redraw_needed(见 base.lua 的make_widget实现),同时初始化widget::layout_changed/widget::redraw_needed/button::press/button::release/mouse::enter/mouse::leave等信号的语义。 - 鼠标输入:
make_widget会自动连接button::press与button::release信号到内部按钮分发器base.handle_button,并设置基础状态:visible = true、opacity = 1、is_widget = true、forced_width/forced_height = nil。 - 通用属性:所有 widget 共享的属性在这里被初始化,包括
children、all_children、forced_width、forced_height、opacity、visible与buttons。
创建后,返回的 widget 拥有一个:buttons成员函数,用于向 widget 注册一组鼠标按钮事件(通常配合awful.button使用)。从源码结构看,make_widget还会把base.widget表中的全部函数(如add_button、set_visible、get_all_children等)复制到新对象上。
make_widget的完整签名支持三个可选参数:make_widget(proxy, widget_name, args)。其中proxy用于创建一个代理 widget(外观与被代理 widget 完全一致,但拥有独立信号生命周期);args表可传入enable_properties(是否启用自动 getter/setter,默认启用)与class。此外还有一个便捷构造器wibox.widget.base.empty_widget(),返回一个不占空间、不绘制任何内容的空 widget。
核心回调一::fit—— 向布局协商尺寸
自定义 widget 需要实现的第一组成员函数是:fit。当布局系统需要确定 widget 应该占据多大空间时,会调用它。参数是当前可用的空间(width、height),返回值是 widget 期望的尺寸。
function widget:fit(context, width, height) -- Find the maximum square available local m = math.min(width, height) return m, m end官方文档特别强调两个要点:
:fit只是"建议"。布局系统不一定采纳它返回的尺寸,因此 widget 必须能够在任何尺寸下正确绘制自己。:fit必须是确定性的(deterministic)。对同样的参数反复调用,必须返回同样的结果。如果 widget 内部状态更新导致:fit的结果将要变化,必须发出widget::layout_changed信号(见下文),让布局系统重新协商,而不是让:fit悄悄返回不同的值。
从实现层面看,base.lua 中的base.fit_widget(parent, context, widget, width, height)是:fit的唯一合法入口。它做了四件事:
- 记录父 widget 对子 widget 的依赖关系(
record_dependency),用于缓存失效的级联; - 若 widget 不可见,直接返回
0, 0; - 将尺寸参数与返回值都裁剪到非负范围(同时过滤 NaN 等非法输入);
- 通过
gears.cache缓存:fit的结果,并在 widget 收到widget::layout_changed时由clear_caches递归清理所有依赖它的缓存。
如果 widget 没有fit方法,fit_widget会退化为基于子 widget 尺寸计算:调用layout_widget布局所有子 widget,取覆盖所有子 widget 所需的最大宽高。因此,纯容器(如wibox.container.background)可以不实现:fit。
核心回调二::draw—— 用 Cairo 绘制内容
:draw是真正把 widget 画上屏幕的回调。参数是绘制上下文(context)、Cairo 上下文(cr)以及 widget 的宽高。
function widget:draw(context, cr, width, height) cr:move_to(0, 0) cr:line_to(width, height) cr:move_to(0, height) cr:line_to(width, 0) cr:stroke() end官方文档为:draw约定了两条关键的坐标系与裁剪规则:
- 坐标系已经就位:Cairo 上下文被设置为 widget 左上角在
(0, 0)、右下角在(width, height),无需做任何额外变换,直接以本地坐标绘制即可。 - 裁剪已被应用:绘制该 widget 时,负责布局它的 layout 已经为其注册了绘制区域,
cr上带有合适的 clip,因此:draw不可能画到自己的注册区域之外。不要调用cr:reset_clip()——否则重绘将无法正确处理。
绘制所需的全部图形能力来自 Cairo:路径(cr:move_to/cr:line_to/cr:arc)、上下文属性(cr:set_source_rgb等)、pattern、变换(transformation)与算子(operator)。也可以使用 Pango 绘制文本。仓库中大量内置 widget 就是这套协议的范例,例如 lib/wibox/widget/textbox.lua、lib/wibox/widget/progressbar.lua 以及 lib/wibox/container/arcchart.lua。
两个核心信号:widget::layout_changed与widget::redraw_needed
自定义 widget 的更新机制完全依赖两个预定义信号(语义定义在 base.lua 的信号注释中):
| 信号 | 触发时机 | 后果 |
|---|---|---|
widget::layout_changed | :fit或:layout的结果将要发生变化 | 重新协商布局,受影响区域被重绘;同时清理gears.cache中该 widget 及所有依赖它的缓存 |
widget::redraw_needed | 仅内容需要重绘,但:fit/:layout的结果不变 | 直接触发:draw重绘,不重新布局 |
官方文档给出的经验法则是:如果拿不准,就把两个信号都发出去,这样永远安全。例如 lib/wibox/container/background.lua 中修改bg时发出widget::redraw_needed,而修改 shape 时同时发出两个信号;lib/wibox/container/border.lua 中set_width/set_color也会按需组合这两个信号。
从底层看,widget::layout_changed与缓存失效强绑定:make_widget中为每个 widget 注册了widget::layout_changed→clear_caches的处理器,而clear_caches会通过依赖表递归清理所有父级缓存。重绘本身则是异步合并的:在 lib/wibox/drawable.lua 中,收到重绘需求后通过timer.delayed_call调度do_redraw,并用_redraw_pending标记保证同一个帧内多次请求只重绘一次。这也解释了为什么:fit必须确定性——重绘是异步批量的,如果:fit结果不稳定,缓存与布局将产生不可预测的连锁变化。
布局类 Widget::layout、:fit_widget与place_widget_at
如果 widget 只是"画点什么",以上内容已经足够。但如果要实现一个布局(layout,即放置其他 widget 的 widget),则需要实现:layout回调。
-- For readability local base = wibox.widget.base function widget:layout(width, height) local result = {} table.insert(result, base.place_widget_at(child, width/2, 0, width/2, height)) return result endbase.place_widget_at(widget, x, y, width, height)返回一个不透明的放置描述表,:layout需要把所有子 widget 的放置信息收集到一张表中返回。官方文档允许把子 widget 放到超出自身范围的位置,例如负坐标或自身尺寸的两倍——当你需要在自身范围外绘制时,就用这个机制。
与:fit对应,这里也有两条实现规则:
:layout的结果变化时,必须发出widget::layout_changed。- 永远不要直接调用其他 widget 的
:fit,必须通过base.fit_widget。因为只有fit_widget会记录父子的依赖关系、处理强制尺寸并走缓存;直接调用会绕过缓存系统,导致重绘失效时无法级联刷新。
从源码看,base.lua 中的place_widget_at实际上是对place_widget_via_matrix的封装,内部通过gears.matrix.create_translate(x, y)生成变换矩阵。而base.layout_widget(parent, context, widget, width, height)是:layout的合法入口,同样带缓存与依赖记录。如果你需要更复杂的放置(旋转、缩放等),可以直接使用base.place_widget_via_matrix(widget, mat, width, height)传入自定义矩阵。
控制子 Widget 绘制:四个钩子回调
当一个布局 widget 要影响子 widget 的绘制方式时,有四个可选的钩子回调,参数与:draw基本一致:
function widget:before_draw_children(context, cr, width, height) function widget:after_draw_children(context, cr, width, height) function widget:before_draw_child(context, index, child, cr, width, height) function widget:after_draw_child(context, index, child, cr, width, height)注意两个差异点:
before_draw_child/after_draw_child额外携带index(子 widget 序号)与child(子 widget 对象)两个参数。- 这四个回调执行期间,Cairo 上下文的 clip 区域更大,覆盖所有子 widget 的区域。它们应当只影响"子 widget 的绘制方式",不应改变绘制覆盖的面积。
官方文档给出最典型的用法——把子 widget 半透明地绘制出来:
function widget:before_draw_children(context, cr, width, height) cr:push_group() end function widget:after_draw_children(context, cr, width, height) cr:pop_group_to_source() cr:paint_with_alpha(0.5) endbefore_draw_children中cr:push_group()把子 widget 的绘制收进一个临时组,after_draw_children中cr:pop_group_to_source()取回该组并用paint_with_alpha(0.5)以 50% 透明度整体刷回,从而实现整组半透明效果。
官方文档用伪代码完整描述了重绘时的调用序列,这也是 lib/wibox/hierarchy.lua 中draw阶段的真实执行顺序(对应源码中before_draw_children→ 逐子before_draw_child/after_draw_child→after_draw_children的调用链):
widget:draw(context, cr, width, height) widget:before_draw_children(context, cr, width, height) for child do widget:before_draw_child(context, cr, child_index, child, width, height) cr:save() -- Draw child and all of its children recursively, taking into account the -- position and size given to base.place_widget_at() in :layout(). cr:restore() widget:after_draw_child(context, cr, child_index, child, width, height) end widget:after_draw_children(context, cr, width, height)仓库中的真实例子包括 lib/wibox/container/arcchart.lua(用before_draw_children/after_draw_children在绘制前后设置与恢复圆弧状态)和 lib/wibox/container/background.lua(通过cr:push_group()/cr:pop_group()实现带圆角或形状裁剪的背景绘制,pop_group弹出的正是before_draw_children里压入的组)。
:set_children:与声明式布局系统的契约
wibox.widget.base中有一个默认实现为空的操作方法set_children:
function base.widget:set_children(children) -- luacheck: no unused -- Nothing on purpose end它扮演的角色很特殊:使用声明式布局系统(wibox.widget { ... }/:setup { ... })设置 widget 时,set_children会被递归调用(见 base.lua 中drill解析声明式表后调用l:set_children(widgets)的代码)。因此官方文档要求:
- 自定义 widget 的
set_children必须定义良好,通常应挂钩到内部的:add/:add_widget方法; - 如果 widget 不接受子 widget,则应把
set_children重写为"什么都不做"。
注意默认实现"什么都不做"与"定义良好"并不矛盾——对无子 widget 的类型而言,默认实现即正确的覆盖。只有那些实际持有子 widget 的布局与容器才需要真正实现它,例如wibox.layout.fixed、wibox.container.background等。声明式布局的完整语法与组合方式见 docs/03-declarative-layout.md。
综合示例:一个可嵌入状态栏的完整自定义 Widget
把上面的协议串起来,可以写一个完整的自定义 widget。以下例子结合了官方文档的:fit/:draw用法与声明式语法(wibox.widget.base.make_widget作为layout字段,意味着这个表会被解析为一个基于make_widget创建的原始 widget,其属性会被设置为fit/draw回调):
-- 一个红色圆形 widget:占据可用高度的正方形区域 local circle = { fit = function(self, context, width, height) return height, height -- 一个占满高度的正方形 end, draw = function(self, context, cr, width, height) cr:set_source_rgb(1, 0, 0) -- 红色 cr:arc(height/2, height/2, height/2, 0, math.pi*2) cr:fill() end, layout = wibox.widget.base.make_widget, } s.mywibox : setup { circle, circle, circle, layout = wibox.layout.fixed.horizontal, }这个例子展示了完整开发流程:layout = wibox.widget.base.make_widget创建骨架 → 声明式解析器把fit/draw表项设置为 widget 的方法 → 布局系统通过base.fit_widget获得尺寸、通过 hierarchy 的绘制管线调用:draw。仓库的示例测试tests/examples/wibox/widget/目录下也存在大量以wibox.widget.base.make_widget()作为占位/骨架 widget 的用例,可作参考。
如果你的 widget 需要随数据变化而更新(例如定期刷新文本、进度条),记住更新协议:
-- 内容变了但尺寸没变 widget:emit_signal("widget::redraw_needed") -- 尺寸也会变(比如文本变长) widget:emit_signal("widget::layout_changed") -- 不确定时两者都发总结
自定义 widget 的完整开发契约可以浓缩为一张清单:
- 创建:用
wibox.widget.base.make_widget创建骨架,必要时提供proxy/widget_name/args;通过:buttons或add_button注册鼠标交互。 - 尺寸:实现确定性的
:fit(context, width, height),通过base.fit_widget查询子 widget 尺寸;结果变化时发widget::layout_changed。 - 绘制:实现
:draw(context, cr, width, height),利用已就位的本地坐标系与 Cairo 绘制;不调用cr:reset_clip();内容变化时发widget::redraw_needed。 - 布局:实现
:layout(width, height)并用base.place_widget_at/base.place_widget_via_matrix返回子 widget 放置表;:layout结果变化时发widget::layout_changed。 - 子绘制:需要影响子 widget 绘制时实现
before/after_draw_children与before/after_draw_child四个钩子,只改变绘制方式、不改变绘制面积。 - 兼容声明式系统:为持有子 widget 的类型正确定义
set_children,否则重写为 no-op。
遵循这套协议,自定义 widget 就能与 Awesome WM 的整个 widget 体系(wibar、awful.popup、awful.tooltip、naughty通知、标题栏)无缝协作,并正确响应屏幕、布局与内容的变化。进一步可阅读 docs/03-declarative-layout.md 掌握声明式组合技巧,以及 docs/16-using-cairo.md 了解 Cairo 绘制的更多细节。
- 操作系统
【免费下载链接】awesome
awesome window manager
相关推荐
RVC语音转换实战指南:低数据量AI变声性能优化深度解析
RVC语音转换实战指南:低数据量AI变声性能优化深度解析 面对语音转换领域中数据稀缺和音色泄漏的技术挑战,Retrieval based Voice Conve
人工智能AI 应用语音音频深度学习终极指南:如何为dh/dht项目自定义回调函数与扩展BitTorrent协议
终极指南:如何为dh/dht项目自定义回调函数与扩展BitTorrent协议 GitHub 加速计划中的 dh/dht 项目是一个实现 BitTorrent D
微信聊天记录如何永久保存不丢失?WeChatMsg 导出备份完整指南
微信聊天记录如何永久保存不丢失?WeChatMsg 导出备份完整指南 手机一换,微信聊天记录说没就没——这不是危言耸听,而是每个微信用户迟早都会撞上的现实。这篇
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考