为不想读源代码的人和 AI 准备的速查 API 参考。 覆盖:
Color/ColorF64、Transform2D、Camera2D、Render2D、RStates及 builder 责任链、相关小类型。 完整的概念讲解(坐标系、Layer、KeyState 边沿、物理/逻辑像素、页池…)见 ENGINE_GUIDE.md。约定:所有坐标单位为世界像素;
Y+指向屏幕下方;layer数值小先画。还有:本项目使用的
wgpu版本为30.0.0,与常用的0.20.0的 API 有诸多不同,建议使用rjw_render::wgpu的重导出
- 0. 统一入口(rjw_krusie)
- 1. Color / ColorF64(颜色)
- 2. Transform2D(变换)
- 3. Camera2D(相机)
- 4. SpriteRect(精灵矩形)
- 5. Render2D(2D 批渲染器)
- 6. RStates 渲染状态与 Builder 责任链
- 7. Clear(清屏意图)
- 8. DynamicAtlas(纹理图集)
- 9. Text(文本渲染)
- 10. 其他常用小类型速查
- 11. UI(rjw_ui)
crate:rjw_krusie(聚合 + 模块化运行时)——整套库一行起步;低层与命名冲突走命名空间。
完整的契约(分层 / 13 条规则 / 责任表 / 简并表 / 旧→新映射 / 实现进度)见
API_DESIGN.md。
use rjw_krusie::prelude::*;
#[derive(Default)]
struct Game;
impl App for Game {
fn config(&self) -> AppConfig { AppConfig::new("my game").size(1280.0, 720.0) }
fn update(&mut self, ctx: &mut Ctx) {
// 逻辑:无帧也执行(后台模拟 / 计时 / 输入状态持续)
if ctx.key(KeyCode::Escape).down_edge() { ctx.exit(); }
// 渲染:守卫在应用里 —— 取不到表面时渲染代码一行不执行
let Some(mut f) = ctx.frame() else { return };
f.draw().solid(SpriteRect::new((-50.0, -50.0), (100.0, 100.0)))
.color(Color::GREEN);
f.submit(&mut Camera2D::full(f.region().size()), Clear::color(Color::rgb(0.1, 0.1, 0.2)));
}
}
fn main() -> Result<(), EventLoopError> { run(Game) }// 按需补充(低层类型 / 自由函数 / 冲突名 / 逃生口)
use rjw_krusie::atlas::RegionRef;
use rjw_krusie::collision::Aabb;
use rjw_krusie::render2d::{CustomDraw, Draw2D, VertexP3U2C4};
use rjw_krusie::gpu::{RenderContext, TEXTURES, MESHES};| 层 | 内容 |
|---|---|
rjw_krusie::prelude |
运行时(App run run_with AppConfig Background Ctx Frame Gfx WindowId Clear Vsync);绘制(Render2D SpriteRect Edges Layer SortMode Cull RStates + 状态枚举与描述符 DepthState/StencilState/SamplerDesc/RasterState);资源(ArcTextureWrapped Rgba8 MeshSpec MeshId);数学/相机(Camera2D Transform2D Rect Vec2 Mat4 glam + 引擎 dpi 类型);颜色;输入(KeyCode MouseButton KeyState ScrollDelta);图集;文本(Text TextStyle TextBuffer Align …);UI(Ui UiState Theme + 常用控件);瓦片 |
| 命名空间 | runtime(app 别名)main gpu render2d transform color atlas text ui tilemap collision(同时保留原 crate 名,如 rjw_krusie::rjw_ui::Ui) |
prelude 零冲突:winit 类型(WindowAttributes / LogicalSize / …)与低层机制
(RenderContext / RenderFrame / PassBuilder / Draw2D / SortKey / VertexP3U2C4 /
TEXTURES / MESHES)都在命名空间里,不进 prelude;引擎自有的 dpi 类型(LogicalSize 等)
在 prelude。不做 as 改名。
裁剪:default-features = false 可只保留运行时 + 2D 绘制(不装 atlas / text / ui / tilemap / collision)。
各 crate 仍可单独使用(
use rjw_2d_render::Render2D;…)——rjw_krusie只是聚合 + 运行时,不改变底层 crate 的 API。
crate:rjw_color
| 函数 | 签名 / 用法 | 说明 |
|---|---|---|
Color::rgba |
Color::rgba(r: f32, g: f32, b: f32, a: f32) |
0..=1 浮点颜色 |
Color::rgb |
Color::rgb(r, g, b) |
alpha=1.0 |
Color::rgba_u8 |
Color::rgba_u8(r: u8, g: u8, b: u8, a: u8) |
0..=255 |
Color::rgb_u8 |
Color::rgb_u8(r, g, b) |
alpha=255 |
| 常量 | Color::RED Color::GREEN Color::BLUE Color::WHITE Color::BLACK 等 |
见 consts 模块 |
use rjw_color::Color;
let red = Color::rgba(1.0, 0.0, 0.0, 0.5);
let green = Color::rgba_u8(60, 200, 80, 255);
let arr: [f32; 4] = Color::WHITE.into();| 函数 | 用法 | 说明 |
|---|---|---|
ColorF64::rgba |
ColorF64::rgba(f64, f64, f64, f64) |
高精度 |
.into() |
let c: wgpu::Color = ColorF64::rgba(...).into(); |
直接转换给 Clear::Color(..)(运行时内部使用) |
crate:rjw_transform
pub struct Transform2D {
pub pos: glam::Vec2,
pub scale: glam::Vec2,
pub rotation: f32,
}| 函数 | 用法 | 说明 |
|---|---|---|
IDENTITY |
Transform2D::IDENTITY |
单位变换 |
with_pos |
.with_pos((x, y)) |
设置位置 |
with_scale |
.with_scale((sx, sy)) |
设置缩放 |
with_rot |
.with_rot(0.5) |
设置旋转(弧度) |
旧的
with_move_by/with_walk_by/with_scale_by/with_rotate_by已删除(with_pos(pos + d)已足够)。 原地移动(&mut self)用move_by(delta)/move_local(delta)。
| 函数 | 说明 |
|---|---|
transform_point(local) |
局部点 → 父/世界点 |
inverse_transform_point(world) |
世界点 → 局部点(命中检测用) |
transform_vec(local_vec) / inverse_transform_vec(world_vec) |
方向向量正 / 逆变换 |
compose(&parent) |
组合父级:parent * self |
compose_inverse(&parent) |
组合父级之逆 |
to_matrix() |
列主序模型矩阵(m * vec4(local, 0, 1) == transform_point(local);GPU / 剔除 / 相机统一出口) |
inverse() |
逆变换对象(⚠ 非均匀缩放 + 旋转下与 inverse_transform_point 的点级精确逆不同) |
💡 旋转中心 = pos:让精灵绕自身中心转,矩形写成
SpriteRect::new((-w / 2.0, -w / 2.0), (w, w))。
crate:rjw_transform
pub struct Camera2D {
pub region: Rect, // 画面矩形(屏幕像素,左上原点)
pub transform: Transform2D, // 相机在世界中的位姿:pos / rotation / scale
}
// Camera2D: Deref/DerefMut<Target = Transform2D> ⇒ `cam.move_by(..)` 等位姿方法直接用
// transform.scale = 世界单位/像素;`zoom()`(越大越放大)是派生视图 = 1 / scale| 函数 | 用法 | 说明 |
|---|---|---|
Camera2D::new |
Camera2D::new(Rect::new(0.0, 0.0, w, h)) |
以画面矩形建相机(位姿 = IDENTITY) |
Camera2D::full |
Camera2D::full((w, h)) |
全窗口画面(等价 new(Rect::new(0, 0, w, h))) |
set_region / region() |
cam.set_region(f.region()) |
画面矩形读 / 写(submit 会写回相机的 region) |
高 DPI 下用物理像素(
f.region()/Gfx::size()已经是物理像素)。
| 函数 | 用法 | 说明 |
|---|---|---|
move_by |
cam.move_by((dx, dy)) |
世界坐标平移(不随旋转) |
move_local |
cam.move_local((lx, ly)) |
沿相机自身方向移动 |
| 函数 | 说明 |
|---|---|
vp_matrix() |
列主序 VP(P×V):由 Frame::submit(&mut cam, clear) / Render2D::submit(&mut pass, &cam) 自动取用(无 set_mvp) |
view_matrix() / projection_matrix() |
世界 → 画面居中像素 / 画面居中像素 → NDC(含 Y 翻转) |
screen_to_world(screen_px) |
窗口像素 → 世界 |
world_to_screen(world) |
世界 → 窗口像素(与上者互为精确逆) |
world_to_region_local(world) |
世界 → 画面居中像素(不含 region 偏移;屏幕固定绘制用) |
view_half_size() / view_aabb() |
可见半宽高(世界单位)/ 世界视口保守 AABB(含旋转,剔除不误杀) |
zoom() / set_zoom(zoom) |
派生缩放视图(越大越放大);写入 transform.scale = 1/zoom |
// 鼠标指向的世界点
let world = cam.screen_to_world(f.mouse().pos_px());crate:rjw_2d_render(data 模块)
pub struct SpriteRect {
pub mesh_tl: Vec2, // 世界坐标左上角
pub mesh_wh: Vec2, // 世界尺寸
pub uv_tl: Vec2, // 归一化 UV 左上 (0..1)
pub uv_wh: Vec2, // 归一化 UV 尺寸 (0..1)
}| 函数 | 用法 | 说明 |
|---|---|---|
new |
SpriteRect::new(tl, wh) |
★整张纹理铺满(最常用) |
centered |
SpriteRect::centered(center, wh) |
以中心点 + 尺寸 |
with_uv |
SpriteRect::with_uv(tl, wh, uv_tl, uv_wh) |
归一化 UV 全指定 |
with_uv_px |
SpriteRect::with_uv_px(tl, wh, uv_tl_px, uv_wh_px, tex_wh) |
像素 UV 子区(给纹理尺寸,无需算倒数) |
with_uv_tex |
SpriteRect::with_uv_tex(tl, wh, uv_tl_px, uv_wh_px, &tex) |
像素 UV 子区(尺寸自动取自纹理) |
at / move_by |
r.at(pos) / r.move_by(delta) |
移动(保持尺寸与 UV) |
size |
r.size(wh) |
改世界尺寸 |
uv / uv_px |
r.uv(tl, wh) / r.uv_px(tl_px, wh_px, tex_wh) |
改 UV(归一化 / 像素) |
shrink / shrink_uv |
r.shrink(4.0) / r.shrink_uv((0.02, 0.0)) |
世界矩形 / 归一化 UV 各边收窄(f32 四边同值 / (x, y) 分轴 / Edges 逐边;负值即外扩) |
位置 / 尺寸 / UV 参数接受
Vec2或(x, y)(正方形写(x, x))。 v0.2.0 起:new即"整张纹理";需要子区用with_uv*(旧from_texture/from_texture_px已删除)。
use rjw_2d_render::SpriteRect;
use glam::Vec2;
let a = SpriteRect::new(Vec2::ZERO, (32.0, 32.0)); // 整张纹理
let b = SpriteRect::with_uv_tex(Vec2::ZERO, (32.0, 32.0), (8, 8), (16, 16), &tex);
let c = SpriteRect::centered(pos, (48.0, 48.0)); // 以 pos 为中心
let d = a.shrink(4.0).at((100.0, 50.0)); // 链式:四周各收 4px 后移动crate:rjw_2d_render(data 模块)
v0.3.0 起:
SpriteRectPx已删除(API_DESIGN.md§5「精灵矩形」简并)。 "世界矩形 + 像素 UV"由SpriteRect::with_uv_px/with_uv_tex直接表达,内部归一化, 不需要自己算1/尺寸;图集精灵直接用AtlasSprite。
use rjw_2d_render::{Edges, SpriteRect};
// 整张贴图(尺寸显式给出)
let base = SpriteRect::with_uv_px(Vec2::ZERO, (64.0, 64.0), Vec2::ZERO, (64.0, 64.0), tex_wh);
// 子区 (8,8)-(24,24),纹理尺寸取自纹理
let sub = SpriteRect::with_uv_tex(Vec2::ZERO, (64.0, 64.0), (8, 8), (16, 16), &tex);
// 已有矩形改像素 UV
let other = SpriteRect::new(pos, (32.0, 32.0)).uv_px((8, 8), (16, 16), tex_wh);裁剪量 Edges(每边各自的量,不是总量):
| 构造 | 说明 |
|---|---|
Edges::all(v) / Edges::xy(x, y) / Edges::lrtb(l, r, t, b) |
四边同值 / 左右上下 / 逐边 |
Edges::new().left(8.0) |
链式只改某一边 |
直接传 f32 / (x, y) / Vec2 / [f32; 2] |
等价 all(v) / xy(x, y) |
| 调整 | 用法 | 说明 |
|---|---|---|
shrink |
rect.shrink(4.0) |
世界矩形各边收窄(UV 不动;负值即外扩) |
shrink_uv |
rect.shrink_uv(Edges::xy(0.125, 0.25)) |
归一化 UV 各边收窄(参数为 0..1 比例;负值即外扩) |
收窄不 clamp:过窄 / 越界由调用方负责(引擎希望"所见即所写",避免隐藏的边界修正)。
crate:rjw_2d_render(v0.2.0 起 API 重新设计:统一 Builder 责任链 + 唯一状态入口 + 排序/剔除分离)
生命周期:
Render2D::new(&RenderContext)持有 surface 的'static引用,要求RenderContext比Render2D更久。设计原则:入口少参数(≤ 2);"在哪层 / 什么色 / 什么变换 / 什么状态"全部走链式; 排序与剔除是对命令索引数组的操作,已独立到
rjw_2d_render::sort/cull(纯函数、可单测、可复用)。
| 函数 | 用法 | 说明 |
|---|---|---|
new |
Render2D::new(&render_ctx) |
基于 RenderContext 创建 |
set_mvp / set_camera |
已删除:相机由调用方持有,submit(&mut pass, &cam) / Frame::submit(&mut cam, clear) 自动取 VP 与画面矩形 |
|
mvp() |
r2d.mvp() |
当前 VP |
texture_layout() |
r2d.texture_layout() |
纹理 bind group layout(rjw_text / rjw_ui 自建 bind group 用) |
device() / queue() |
r2d.device() / r2d.queue() |
暴露底层 wgpu(高级用法) |
| 函数 | 说明 |
|---|---|
sort(SortMode) |
LayerAndStates(默认,按 (layer, states) 排序合批)/ LayerOnly(仅按 layer 稳定排序,同层保录制序,UI 适用)/ None(完全按录制顺序) |
sort_custom(Box<dyn SortPolicy>) |
注入自定义排序策略(fn sort(&self, order: &mut [usize], keys: &[SortKey])) |
sort_mode() -> SortMode |
当前内置模式 |
SortKey { layer, rstates, texture_uid }:与命令下标对齐的排序键(keys[order[i]]才是第i条命令的键)。SortMode::apply(order, keys)/SortPolicy::sort(..):纯函数,只重排索引数组,命令数据不动。
| 函数 | 说明 |
|---|---|
cull(impl Into<Cull>) |
单一入口:Cull::Off(默认)/ Cull::Viewport / Cull::Rect(rect) / Cull::Fn(Box<dyn Fn(&Rect) -> bool>) / Cull::from(&cam)(以相机剔除,取代旧 set_cull_camera) |
cull_mode() / culler() / culler_mut() |
当前模式 / 剔除器(下游可复用同一套可见性判定) |
Culler::new(Cull::...)、visible(&Rect) -> bool、retain(&mut Vec<usize>, aabb_of)(就地过滤索引数组)。- 纯几何:
sprite_world_aabb(&SpriteRect, &Mat4)、viewport_world_rect(&Mat4)、transform2d_model(&Transform2D)。 - 行为:只有 Sprite 命令参与剔除(动态 Mesh / StaticMesh / Custom 恒保留)。
| 函数 | 说明 |
|---|---|
states_mut(RStates) |
设置全局默认状态(未链式设置状态的命令继承它) |
states() -> RStates |
当前全局默认状态(只读) |
reset() |
重置画面矩形 / scissor / 全局默认状态 / 剔除(一体复位) |
use rjw_2d_render::{AddressMode, BlendMode, CompareFunc, RStates};
r2d.states_mut(
RStates::new()
.blend(BlendMode::Additive)
.depth_test(true)
.depth_write(true)
.depth_compare(CompareFunc::Less)
.samp_addr_all(AddressMode::Repeat),
);旧版的 21 个
default_*方法已删除——RStates本身就是链式状态语言,只需一个入口。
| 函数 | 用法 | 说明 |
|---|---|---|
sprite(rect, &tex) |
r2d.sprite(rect, &tex).tint(c).transform(tf).layer(l) |
贴纹理精灵(实例化合批) |
solid(rect) |
r2d.solid(rect).tint(c).layer(l) |
纯色精灵(内部 1×1 白纹理) |
region(AtlasSprite) |
r2d.region(spr).layer(l) |
★ 图集直达(atlas.sprite(&handle) 的产物:区域 + 页纹理一次拿到) |
mesh(&verts, &tris) |
r2d.mesh(&verts, &tris).tint(c).transform(tf) |
显式顶点 + u16 索引(世界坐标) |
mesh_with(|sink| ..) |
r2d.mesh_with(|s| { s.push_tri(a,b,c); }).tint(c) |
流式构造(自定三角化 / 逐顶点 UV) |
polygon(&verts) |
r2d.polygon(&verts).tint(c).layer(l) |
多边形(fan 三角化:首顶点为中心) |
polygon_with(|p| ..) |
r2d.polygon_with(|p| { p.vertex(a); p.vertex_uv(b, uv); .. }) |
流式多边形(自动 fan 三角化;带 UV 用它) |
quads(&verts, &tex) |
r2d.quads(&verts, &tex).transform(tf).tint(tint) |
四边形段(顶点 TL,TR,BL,BR;.tint() 为整段实例色) |
quads_with(|q| .., &tex) |
r2d.quads_with(|q| { q.quad(tl,tr,bl,br); }, &tex) |
流式四边形段 |
static_mesh(MeshId, &tex) |
r2d.static_mesh(id, &tex).tint(c).transform(tf).layer(l) |
静态网格实例(句柄来自 Gfx::mesh;MESHES 注册表 + 实例化合批) |
custom(cd) |
r2d.custom(|pass| { .. }).layer(l) |
注入原生 wgpu 绘制(CustomDraw / 闭包 blanket impl) |
入口命名规则:
kind(数据…)= 已有数据直接给;kind_with(|sink| …)= 流式构造(零临时Vec)。 旧版 16 个add_*与mesh_with_cap/polygon_uv已删除; 需要预留容量请在闭包外自行Vec::with_capacity(mesh(&verts, &tris)直接给已构造好的数据), 带 UV 的多边形用polygon_with的vertex_uv(..)。
| 方法 | 默认 | 说明 |
|---|---|---|
.layer(impl Into<Layer>) |
0.0 |
绘制层级(数值小先绘制;接受 f32 / i32 / u32 / f64) |
.tint(Color) |
WHITE |
Sprite/StaticMesh:实例色;mesh/polygon:逐顶点色;quads:整段实例色 |
.transform(Transform2D) |
IDENTITY |
局部 → 世界 |
.at(p) / .rot(r) / .scale(s) |
— | transform 便捷糖 |
.matrix(Mat4) |
— | 直接给列主序模型矩阵(覆盖 transform) |
.texture(&tex) |
入口参数 | 覆盖采样纹理(Mesh 系默认白纹理) |
.states(RStates) |
None = 继承全局 |
完整渲染状态(唯一权威,覆盖此前的糖) |
.blend(BlendMode) / .sampler(SamplerDesc) / .cull(CullMode) / .raster(RasterState) / .depth(impl Into<DepthState>) / .stencil(impl Into<StencilState>) |
— | states 便捷糖;收对象/枚举,不收裸 bool(DepthState::{test_write, test_only, off}) |
// 世界坐标旋转精灵 + 加性混合
r2d.sprite(rect, &tex)
.tint(Color::WHITE)
.transform(Transform2D::IDENTITY.with_rot(t))
.layer(y_layer(foot_y))
.blend(BlendMode::Additive);
// 流式画圆(零临时 Vec)
r2d.polygon_with(|p| {
p.vertex(center);
for i in 0..=22 {
let a = i as f32 / 22.0 * std::f32::consts::TAU;
p.vertex(center + Vec2::new(a.cos(), a.sin()) * r);
}
})
.color(Color::CYAN)
.layer(96.0);
Draw2D<'a, K>是本工程唯一的绘制 Builder(Sprite/Mesh/StaticMesh/Custom四个 kind); 类型别名SpriteBuilder/MeshBuilder/StaticMeshBuilder/CustomBuilder供签名标注。
| 函数 | 说明 |
|---|---|
Draw2D::scissor(rect: Rect) |
命令级 scissor(屏幕/目标像素、左上原点)。最终 = 命令级 ∩ 画面级 Render2D::scissor ∩ 目标矩形;空矩形 ⇒ 该命令整条跳过(0 draw,不是"不裁剪");不同 scissor 的命令不合批(一次 draw_indexed 只能一个 scissor) |
Draw2D::scissor_opt(Option<Rect>) |
同上,None = 不设(继承画面级) |
Render2D::scissor(Option<Rect>) |
画面级 scissor(整画面;浮点、按目标钳制;见 §5.1) |
Render2D::draw_op_count() -> usize |
上一帧 prepare() 后的 draw op 数(= draw call 数),诊断"scissor 让 draw 变多" |
scissor走两级:画面级(Render2D,作用于整个pass)与命令级(Draw2D,逐条命令)。 两者的矩形都在绘制时按像素取整并钳到目标;任一为空/全在目标外则该 op 不发 draw。 UI 用它承载环境裁剪([UiBatch::clip]),从而不再切割几何——圆角、环边、投影 保持原形。见docs/ENGINE_GUIDE.md§18.20。
| 函数 | 说明 |
|---|---|
Gfx::mesh(MeshSpec { .. }) -> MeshId |
建静态网格(&Gpu 工厂:gfx.mesh(..) 或 gpu.mesh(..)),返回可复用句柄 |
static_mesh(mesh_id, &tex).tint(..).transform(tf).layer(..) |
提交一个实例(CPU 侧只有变换 + 颜色) |
static_mesh(id, &tex).matrix(mat) |
直接给模型矩阵(跳过 Transform2D 推导) |
- 合批条件:
(mesh_id, rstates, tex_uid)相同且绘制序列连续 → 合并为同一次draw_indexed。 - 适用:固定层级、不参与 y-sort 的地图元素(石头 / 花 / 栅栏);会插入实体排序的(如 y-sort 的树)必须保持动态。
MeshSpec见 [MeshSpec](label/vertices/indices);低层自建用MeshData::from_pod(device, &verts, &indices, label)/from_buffers(vb, ib, index_count)。
| 函数 | 说明 |
|---|---|
submit(&mut PassBuilder, &Camera2D) |
★ 一个画面 = 一个 pass = 一个 VP 槽:写回 cam.region → 取 cam.vp_matrix() → 开 pass → 提交队列 → 清帧 |
Render2D::render(&mut RenderContext, Clear) |
单画面一行糖:acquire_frame → submit(用当前相机)→ present |
discard() |
丢弃未提交的录制(无帧帧 / 主动放弃本帧) |
RenderFrame::pass(clear) / pass_to(target, clear) |
低层:自己开 pass(多次 record 共用一次 Load/Store;离屏走 pass_to) |
PassBuilder::record(&mut R) |
把某个 PassRecorder(如 Render2D)录进本 pass;可多次 |
RenderFrame::present() |
提交 encoder + present(#[must_use]:忘记会记 warning) |
RenderContext::acquire_frame() |
取当前表面帧(None = 取帧失败,跳过本帧;FrameSource 是同一入口的抽象) |
应用层通常不直接碰这些:
rjw_krusie::runtime的Frame::{submit, present}已封装 「取帧 → 相机写回 → 开 pass → 提交 → present」;无帧时Ctx::frame()返回None。
apply_order():take_order() → fill_sort_keys() → SortPolicy::sort() → Culler::retain() → set_order()
prepare() :RStates resolve(None → 全局默认)→ 生成实例 / 动态段 → 按 (layer, mesh_id, rstates, tex) 分组
→ 按 MAX_INSTANCES_PER_DRAW(8192) 分页 → 上传实例页 / 动态顶点缓冲
draw() :按 DrawOp.rstates 取/建管线 → 绑定纹理 bind group → 逐页 draw_indexed
- 实例缓冲是页池:单帧实例数可远超 8192,自动分页;不要自己裁减数量去凑。
- 命令 / 网格 / 排序键缓冲全部常驻复用(
clear()只清长度不释放);Builder 为栈上 struct,无堆分配。
- 创建:
gpu.texture(label, Rgba8::new(&rgba, (w, h))) -> ArcTextureWrapped(RGBA8,len == w*h*4否则 panic); 注册表低层用gpu.textures().register(arc)。 TextureWrapped(rjw_render)只持有纹理本身;采样器完全由RStates位域(bits 8..24)驱动 (.sampler(FilterMode::Nearest, AddressMode::Repeat)或.states(..)),Render2D内部按需创建并缓存wgpu::Sampler。- bind group 按
(tex_uid, samp_key)缓存,value 持有Arc<Texture>防悬挂;prepare末尾自动剔除失效条目。 - 1×1 白纹理:
Gpu::white_texture()/Render2D内部的white_texture(纯色绘制与solid使用)。 - 纹理注册表是每
RenderContext私有的(不再是全局static):gpu.textures()(&TextureRegistry)/r2d.textures()(绘制期);需要长期持有用gpu.texture_registry()(&Arc<..>)。 操作:register/register_named/get/remove/rename/contains_uid/contains_name。v0.3 起
rjw_render::TEXTURES/MESHES两个全局static已删除。旧全局单例下Render2D::new每次注册的 1×1 白纹理与四边形网格会覆盖先建渲染器的条目 (同一 uid 指向别人的纹理);实例化后跨RenderContext彻底隔离。 uid 仍由进程级计数器保证全局单调不复用,但uid 相等不再蕴含「同一张纹理」。
crate:rjw_2d_render(rstates 模块)
RStates 是 u64 bitfield,涵盖 6 个控制域:Blend / Sampler / Cull+Raster / Depth / Stencil / Reserved。
| 分类 | 方法 | 说明 |
|---|---|---|
| Blend | blend(BlendMode) / blend_state(BlendDesc) |
Alpha/Additive/Multiply/Premultiplied/Inverse/Subtract/Min/Max/Disabled |
| Sampler | samp_mag(f) / samp_min(f) / samp_mip(f) |
Linear / Nearest |
samp_addr_u(a) / samp_addr_v(a) / samp_addr_w(a) |
ClampToEdge / Repeat / MirrorRepeat | |
samp_state(SamplerDesc) |
批量设置采样器 | |
| Cull+Raster | cull(CullMode) / polygon(PolygonMode) / front_face(FrontFaceWinding) / conservative_raster(bool) |
None/Front/Back; Fill/Line/Point; Ccw/Cw |
raster_state(RasterState) |
批量设置光栅化 | |
| Depth | depth_test(bool) / depth_write(bool) / depth_compare(CompareFunc) |
Less/LessEq/Greater/... |
depth_state(DepthState) |
批量设置深度 | |
| Stencil | stencil_test(bool) / stencil_write(bool) / stencil_compare(CompareFunc) |
Always/Never/... |
stencil_state(StencilState) |
批量设置模板 |
| 分类 | 方法 |
|---|---|
| 通用修饰 | .layer(impl Into<Layer>) / .tint(Color) / .transform(tf) / .at(p) / .rot(r) / .scale(s) / .matrix(mat) |
| 纹理 | .texture(&tex)(Mesh 系默认白纹理;Sprite/StaticMesh 由入口参数给出) |
| 状态(全量) | .states(RStates)(唯一权威;后写覆盖前写) |
| 状态(对象糖) | .blend(BlendMode) / .samp(FilterMode, AddressMode) / .cull(CullMode) / .depth(impl Into<DepthState>) / .stencil(impl Into<StencilState>) |
| 状态(描述符) | .blend_state(BlendDesc) / .samp_state(SamplerDesc) / .raster_state(RasterState) |
| 提交 | .done()(显式提交;亦可依赖 Drop 自动 push) |
💡 需要
RStates的完整表达能力(polygon / front_face / conservative_raster 等)时,用.states(RStates::new().polygon(PolygonMode::Line))——状态只有一个语言:RStates。
不链式调用 = 继承全局默认(Render2D::states());链式任一项 = 该命令显式状态。
| 类型 | 值 |
|---|---|
RStates |
u64 bitfield,RStates::default() / new() = 全零(默认) |
BlendMode |
Alpha / Additive / Multiply / Premultiplied / Inverse / Subtract / Min / Max / Disabled |
FilterMode |
Linear / Nearest |
AddressMode |
ClampToEdge / Repeat / MirrorRepeat |
CullMode |
None / Front / Back |
PolygonMode |
Fill / Line / Point |
FrontFaceWinding |
Ccw / Cw |
CompareFunc |
Never / Less / Equal / LessEq / Greater / NotEq / GreaterEq / Always |
BlendDesc |
{ blend_mode: BlendMode } |
SamplerDesc |
{ mag, min, mip: FilterMode, addr_u, addr_v, addr_w: AddressMode } |
RasterState |
{ cull: CullMode, polygon: PolygonMode, front_face: FrontFaceWinding, conservative: bool } |
DepthState |
{ test: bool, write: bool, compare: CompareFunc } |
StencilState |
{ test: bool, write: bool, compare: CompareFunc } |
MeshData |
静态网格:{ vertex_buffer, index_buffer, index_count, uid }(rjw_render) |
StaticMeshBuilder<'a> |
static_mesh 返回(Drop 即提交,或 .done() 显式) |
HasUid |
trait:fn uid(&self) -> u64(rjw_render) |
TypedRegistry<T: HasUid> |
泛型注册表:register / register_named / get / get_ref / remove / remove_name_mapping / rename / contains_uid / contains_name |
MeshRegistry |
静态网格注册表(TypedRegistry<MeshData>);每 RenderContext 私有,经 gpu.meshes() / r2d.meshes() / gpu.mesh_registry() 取得 |
use rjw_2d_render::{BlendMode, FilterMode, AddressMode, DepthState, CompareFunc, RStates};
use rjw_krusie::prelude::*;
// 不链式 = 继承全局默认状态(`r2d.states()`)
f.draw().sprite(rect, &tex);
// 单条链式覆盖(对象糖)
f.draw().sprite(rect, &tex)
.samp(FilterMode::Nearest, AddressMode::Repeat)
.blend(BlendMode::Additive);
// Mesh + 纹理 + 渲染状态
f.draw().polygon(&verts)
.texture(&tex)
.blend(BlendMode::Multiply)
.layer(96.0)
.tint(Color::CYAN);
// 深度状态(对象糖;`DepthState` 有 test_write / test_only / off 等构造)
f.draw().sprite(rect, &tex)
.depth(DepthState { test: true, write: true, compare: CompareFunc::Less });
// 全量状态(唯一状态语言)
f.draw().solid(rect).states(RStates::new().blend(BlendMode::Additive).depth_test(true));
// 全局默认状态(唯一入口)
r2d.states_mut(RStates::new().blend(BlendMode::Additive).depth_test(true).depth_write(true));crate:rjw_render(v0.3 起取代三 Option 的 ClearConfig)
pub enum Clear {
Keep, // 全部保留(叠加画面 / 多 pass)
Color(ColorF64), // 只清颜色
ColorDepth(ColorF64, f32), // 颜色 + 深度
Depth(f32), // 只清深度(保留颜色;重叠画面的独立 pass)
Stencil(u32), // 只清模板
}
impl Clear {
pub fn color(c: impl Into<ColorF64>) -> Self; // 接受 Color 或 ColorF64
pub fn color_depth(c: impl Into<ColorF64>, depth: f32) -> Self;
pub fn depth(depth: f32) -> Self;
pub fn stencil(value: u32) -> Self;
pub fn uses_depth(&self) -> bool;
pub fn uses_stencil(&self) -> bool;
}// 一个画面 = 一次 submit(相机, clear)
f.submit(&mut cam, Clear::color(Color::rgb(0.1, 0.1, 0.2)));
// 重叠画面只清深度
f.submit(&mut cam2, Clear::depth(1.0));
// 独立用法(自带渲染上下文时)
r2d.render(&mut render_ctx, Clear::color(Color::BLACK));深度 / 模板附件是否需要绑定由
PassBuilder从「已排队的 recorder + clear」自动推导, 调用方不再手算need_depth_stencil。
crate:rjw_atlas
pub struct AtlasConfig { pub max_pages: usize, pub padding: u32, pub lifetime: u32, pub page_size: u32 }
pub struct AtlasRegion { pub tl_px: (u32,u32), pub wh_px: (u32,u32), pub origin_px: (u32,u32), pub page_uid: u64 }
pub struct AtlasSprite { pub region: AtlasRegion, pub texture: ArcTextureWrapped } // 可直接绘制
pub struct InsertOpts { /* origin_px / clamp_margin / permanent */ }
pub struct AtlasStats { /* pages / page_size / total_free / largest_free / fragmentation_percent / generation */ }
pub struct DynamicAtlas<K = String>
pub struct StaticAtlas<K = String>💡
DynamicAtlas/StaticAtlas均实现Index<&Q>/IndexMut<&Q>(K: Borrow<Q>):atlas[&key]/atlas["name"]直接读写区域。
| 方法 | 说明 |
|---|---|
DynamicAtlas::new(gfx, config) |
★ 创建空图集(gfx: &Gpu;页尺寸在 config.page_size)。页纹理注册进该 Gpu 的纹理注册表(不再是全局) |
insert(key, Rgba8) |
★ 最常用:Rgba8::new(&rgba, (w, h)),默认 clamp_margin、非常驻、原点 (0,0) |
insert_with(key, Rgba8, InsertOpts) |
指定原点 / no_clamp() / permanent() |
insert_dynamic(key, size, SpriteSource) |
动态再生精灵(复活时调生成器) |
white() |
1×1 白像素(与字形同页 → UI 实心填充可合批) |
region(key) |
查找(会刷新寿命,不触发复活) |
region_or_revive(key) |
★ 查找;若被逐出则自动复活 |
handle(key) |
取 RAII 稳定句柄 RegionRef(保活 + 重排后仍有效) |
sprite(&handle) |
★ 解析成 AtlasSprite(区域 + 页纹理)→ r2d.region(spr) 一次提交 |
tick() |
寿命-1(引擎每渲染帧调用);有源数据→墓碑,可复活 |
stats() |
AtlasStats(页数 / 空闲 / 碎片度 / 世代) |
compact() |
去碎片:带源条目全量重排到最少页(重传纹理,generation+1) |
generation() |
重排世代号(搬动条目时 +1;缓存区域者据此刷新) |
page_size() / page_count() |
查询 |
load_toml / export_toml |
TOML 导入 / 导出(feature toml) |
StaticAtlas::from_toml(s, registry) / get(name) |
静态图集(K=String 特化);tex 字段按名在本上下文的纹理注册表里解析 |
crate:rjw_text
基于 cosmic-text 排版 + swash 字形光栅化 + DynamicAtlas 字形缓存。
pub struct Text { /* font_system, scale_context, glyph_cache: DynamicAtlas<AtlasKey>, locations, ... */ }字形图集把「字形 + UI 自定义纹理 + WHITE 基础纹理」放在同一页,因此它们 纹理相同 → 后端可合批(省掉图形↔文字的纹理状态切换)。它现在是公开可用 / 可修改的:
| 入口 | 说明 |
|---|---|
Text::glyph_cache() -> &DynamicAtlas<AtlasKey> |
只读:page_count() / stats() / generation() / region(&key) |
Text::glyph_cache_mut() -> &mut DynamicAtlas<AtlasKey> |
可变:插入自定义纹理、compact() 去碎片、低级查询 |
Text::user_texture(id: u64, px: Rgba8) -> Option<AtlasRegion> |
★ 推荐路径:固定用 AtlasKey::Custom(id) 命名空间 + permanent()(不被逐出、不撞字形键) |
Text::white_region() -> Option<AtlasRegion> |
1×1 白纹理 region(UI 实心填充 / 边框 / 光标采样;每次调用刷新寿命) |
Text::tick() |
寿命推进(引擎每渲染帧调用;用户不必手动) |
AtlasKey 的两个命名空间必须分清:
pub enum AtlasKey {
Glyph(cosmic_text::CacheKey), // rjw_text 内部字形命名空间——消费者不要写
Custom(u64), // 消费者命名空间(定长去重键,如圆角半径)
}使用 glyph_cache_mut() 的四条约定(违反会静默错位,不报错):
- 不要写
AtlasKey::Glyph(..)—— 那是光栅化逻辑的命名空间,会被覆盖 / 误判命中。 - 不要手改 WHITE 条目 —— 它是 UI 实心填充与字形合批的基础。
- 插入非
permanent()的条目会被tick()的寿命机制逐出;自己长期持有 region 的调用方请用permanent(),或改用handle()走 RAII 保活。 - 图集重排后
AtlasRegion失效 —— 用generation()变化判定并重新region()取。
缓存键与命名空间隔离有回归测试锁定:
rjw_text::tests::atlas_key_namespaces_are_distinct。
⚠ 下表中
draw_label*/TextLayout/TextRender/render_from/Text::new(device, queue, layout)/create_buffer*/measure/visual_lines都是 v0.2 的旧名,v0.3 已删除或改名为Label责任链 (Text::label(..).size(..).at(..).draw(layer))——现行写法见examples/eg260810TextChain与本文档 §9.3。
| 方法 | 说明 |
|---|---|
Text::new(gfx: &Gpu) |
创建字体管理器(自动加载系统字体) |
load_font_data(data: Vec<u8>) -> Vec<String> |
加载额外的 ttf/otf/ttc 字体数据,返回本次新增的字体族名(去重;空 = 该族已在库里)。应用"导入字体"时必须拿到族名才能用(label.font_family(name) 只认族名)——见 Text::font_families(内部求差,与 fontdb 的槽位顺序无关) |
Text::label(text) -> Label |
★ 责任链入口 → .size/.line_height/.align/.font_family/.at/.center/.anchor/.draw(layer) |
Text::label_from(&Arc<Buffer>) -> Label |
从用户保存的共享 Arc<Buffer> 进入(跳过整形) |
Text::measure_buffer(buffer) -> Vec2 |
已排版 Buffer 的内容宽高(空文本返回 (0,0)) |
Text::lines(buffer) -> Vec<VisualLine> |
视觉行(自动换行后):(byte_start, byte_end, top, width)——光标/点击/选择与显示对齐 |
Text::buffer(..) / Text::geometry(..) |
UI 稳定集成面(预排版 / 几何) |
white_region() / user_texture(..) / tick() |
见 §9.1 |
性能:
Text内置排版缓存(LRU)——按(文本/字号/行高/对齐/attrs)缓存 cosmic-text 排版, 相同输入经 O(1) 签名预过滤后返回共享Arc<Buffer>(不深拷贝,跳过重复整形; 上限MAX_LAYOUT_CACHE= 128,满时淘汰最久未用)。缓存启用规则:Debug 恒缓存; Release 仅缓存 ≤LARGE_TEXT_CACHE_LIMIT= 512 字节的小文本(大文本多为动态/低频, 不入缓存、每帧直接整形);静态大文本请保存Arc<Buffer>经label_from手动复用。 空格等无图字形只判定一次(no_image);字形图集去碎片重排后自动同步区域。
use rjw_krusie::prelude::*;
// 世界层文本(`f.text` 绑定世界层渲染器)
f.text(|t| {
t.label("HP 100").size(16.0).at(world_pos).draw(10.0);
});
// 屏幕固定文本(`f.text_ui` 绑定 UI 层,物理像素、左上原点)
f.text_ui(|t| {
t.label("FPS 60").size(14.0).at((16.0, 16.0)).color(Color::YELLOW).draw(0.0);
});
// 往字形图集插自定义纹理(与字形同页 → 同纹理合批)
f.text(|t| {
// 由任意 RGBA 像素(此处 32×32,自行填充)构造
let px = vec![255u8; 32 * 32 * 4];
let region = t
.glyph_cache_mut()
.insert_with(
rjw_krusie::text::AtlasKey::Custom(0xABCD),
rjw_krusie::gpu::Rgba8::new(&px, (32, 32)),
rjw_krusie::atlas::InsertOpts::new().permanent(),
);
let _ = region;
});注(v0.4):
rjw_ui::proc(圆角 9-patch 程序化纹理:rounded_rect_rgba/rounded_9patch/ROUNDED_TEX_SIZE)已删除。圆角不再走纹理,改由 CPU 镶嵌成 三角形(见「渲染增强」一节),因此上面这个示例改为完全自备像素。
| 类型 / 函数 | 位置 | 用途 |
|---|---|---|
KeyState::pressed()/released() |
rjw_keystate |
按住/松开 |
KeyState::down_edge()/up_edge() |
rjw_keystate |
按下/松开那一帧 |
KeyState::true_edge()/down_true_edge() |
rjw_keystate |
系统级真实边沿 |
KeyCode::KeyW/... |
rjw_main 重导出 winit |
键盘常量 |
MouseButton::Left/... |
winit | 鼠标按钮 |
ctx.timer.dt().get_f32() |
rjw_time |
帧间隔秒 |
ctx.timer.get_fps() |
rjw_time |
FPS |
ArcTextureWrapped.uid |
rjw_render |
纹理唯一 ID |
SpriteBuilder<'a> |
rjw_2d_render |
sprite / solid 返回 |
MeshBuilder<'a> |
rjw_2d_render |
mesh* / polygon* / quads* 返回 |
StaticMeshBuilder<'a> |
rjw_2d_render |
static_mesh 返回,Drop 即提交 |
Draw2D<'a, K> |
rjw_2d_render |
唯一 Builder 本体(kind 标记与 DrawKind 为内部机制,#[doc(hidden)]) |
SortMode / SortPolicy / SortKey |
rjw_2d_render |
排序(对索引数组重排) |
Cull / Culler |
rjw_2d_render |
剔除(对索引数组过滤 + 可见性判定) |
MeshData |
rjw_render |
静态网格(GPU 顶点/索引 + uid) |
HasUid |
rjw_render |
全局唯一 id trait |
TypedRegistry<T> |
rjw_render |
泛型线程安全注册表(纹理/网格共用) |
CustomDraw |
rjw_2d_render |
外部绘制 trait(闭包 blanket impl) |
CustomBuilder<'a> |
rjw_2d_render |
custom 返回,可链式 RStates |
crate:
rjw_ui。坐标一律屏幕像素(左上角原点、Y+ 向下);交互状态经 ID 持久化于UiState(应用持有)。完整概念见 ENGINE_GUIDE.md「18. UI」。
一帧 = 开场 + N 段 + 收尾("ui anywhere"):运行时入口
Frame::ui(theme)返回一段 (UiSession,Deref到Ui),一帧可调用任意多次、位置随意(世界绘制之前 / 之间 / 之后均可),&mut Ui可透传给任意函数。帧级账(帧号 / 命中区翻页 / 输入快照 / 焦点导航 + 描边 / 光标 / 统计)每帧只做一次:开场在第一段懒执行(应用常先debug_inject_mouse,快照早了会丢点击边沿),收尾由运行时在 UI 队列被提交前补齐。
| 函数 | 签名 / 用法 | 说明 |
|---|---|---|
Frame::ui(Theme) |
let mut ui = f.ui(Theme::dark()); … ui.finish(); |
运行时入口(推荐):开一段 UI;段结束(finish()/作用域结束)自动提交到 UI 层渲染器。段存活期间 f 被借用(不能 draw/text/submit),段之间可任意交错。帧收尾(焦点描边 / 光标 / 统计)由运行时在 submit / present / 帧尾自动补齐(幂等),应用无需手动调 |
Ui::begin |
Ui::begin(window, &mut text, &mut state) -> UiInit |
低层段入口;window 用于 IME 候选框定位与光标图标。输入与绘制解耦:输入经 UiInit::capture 快照、相机/渲染器延迟到 Ui::finish 传入。⚠ 帧级账由调用方负责:每帧一次 UiState::begin_frame()(build() 会兜底) |
UiInit::capture(&MouseInput, &KeyboardInput) |
.capture(ctx.mouse(), ctx.keys()) |
把键盘/鼠标设备状态拷贝为 Ui 自持快照(省略 = 空输入,headless 安全)。本帧第一段冻结一次,后续段复用(段间注入只影响下一帧) |
UiInit::theme(Theme) / Ui::theme() / Ui::theme_mut() |
.theme(Theme::dark()) |
主题(默认浅色;Theme::dark() 深色;构建后经 ui.theme() / theme_mut() 读写) |
UiInit::base_layer(f64) |
.base_layer(1e7) |
基层层级(默认 1e7) |
UiInit::scale_factor(f64) |
.scale_factor(ctx.scale() as f64) |
DPI:控件坐标/字号按逻辑像素,内部换算物理像素(默认 1.0) |
UiInit::debug_layout() / without_debug_layout() |
.debug_layout() |
调试 UI 布局:给每个控件/容器矩形画描边(颜色/宽度见 样式小节 的 DebugStyle;默认关闭)。无裸布尔:开 = 调 debug_layout(),关 = without_debug_layout() |
Ui::debug_layout() / Ui::without_debug_layout() |
ui.debug_layout() |
同 UiInit 版本,段内运行时开关 |
UiInit::build() |
→ Ui |
完成构建(本帧未开场则顺带开场:帧号 +1 / 命中区翻页 / 输入快照冻结 / 责任链种入) |
Ui::finish(&mut dyn UiBackend) |
ui.finish(&mut backend) |
段收尾:按 (win, depth, 图形/文字, 录制序) 序免全量排序提交(win + depth 分桶、桶内保持录制序);产出 UiBatch 批次交给后端;段统计累加进帧级暂存。一帧可多次 |
Ui::end_frame(&mut dyn UiBackend) |
ui.end_frame(&mut backend) |
帧收尾(每帧一次):输入结算(空白清焦点 / 清一次性边沿 / 窗口按下裁决)/ 焦点导航 + 描边 / 光标定夺 / 统计写回(UiStats.frame 每帧 +1、ui_frame_us = 开场→收尾)/ 帧级暂存关场 |
UiState::new() |
应用持有 | 跨帧持久状态容器 |
UiState::begin_frame() / frame_open() |
运行时内部 / 诊断 | 每帧开场一次(帧号 / 命中区翻页 / 帧级暂存清零);frame_open() 判断本帧是否录过 UI |
UiState::reset() / remove(id) |
示例"R 重开" | 清空全部 / 移除单个控件状态。reset() 逐个转调模块 reset_*()(见下),所以"某模块新增字段忘了清"不可能发生 |
UiState 的 9 个模块 |
state.frame() / .widgets() / .windows() / .texts() / .scrolls() / .popups() / .hits() / .caches() / .stats() |
公开只读视图(模块化的公开面;内部字段布局与应用解耦)。例:state.widgets().sizes()、state.windows().z(id)、state.popups().menu_open()、state.hits().occluded_hits()、state.scrolls().get(id)。写走语义化方法:sizes_mut() / color_picker_mut() / set_menu_open() / set_combo_open() / close_popups()(+既有的 widget(id) / set_collapsed(..)) |
UiState::reset_frame / reset_widgets / reset_windows / reset_texts / reset_scrolls / reset_popups / reset_hits / reset_caches |
只清一个模块 | 模块级重置(reset() = 逐个调用它们 + 清 stats)。⚠ widget_strs 曾漏清——由单测 reset_clears_every_module_after_dirtying 抓出并修掉 |
UiState::text_focus() -> Option<TextFocus> |
if ui.state().text_focus().is_none() { /* 快捷键 */ } |
文本焦点(只有输入框/多行框持焦点才为 Some);取代旧 capturing_text() —— 按钮/滑块的 Tab 焦点不再吞应用快捷键 |
UiState::combo_open() -> Option<&str> |
if ui.state().combo_open().is_none() && esc { /* 自己的 Esc */ } |
当前展开的下拉菜单(Dropdown 的控件绝对 ID;面板窗口 id = 它 + ::popup)。与 menu_open() 对称(两个槽分开存:栏是应用级 UI,下拉属于某个控件);reset() 清空 |
Ui::debug_dump() -> UiDebugDump |
eprintln!("{}", ui.debug_dump()) |
引擎侧状态快照(每窗口 id/z/origin/submit/size/drag/press/stored),单行可 grep;任一段都能调用,帧级暂存跨段共享 ⇒ 后一段能看到前一段录的窗口。⚠ 按本帧录制过的窗口 ID 列(不是按 z):浮层 z = WIN_TOPMOST 基址 + 嵌套层数(子菜单比父面板 +1,ui::overlay_z),按 id 列才不会漏掉嵌套浮层;origin 是相对直接容器的原点(顶层窗口 = 屏幕坐标,嵌套浮层要叠加外层窗口原点)。见 DEBUGGING.md §1 |
绘制器是独立于 Ui 的组件(rjw_ui::painter):它只拥有录制状态(命令队列 +
播放头)与 API 边界的 DPI 换算,不引用 Ui、不需要字体图集 / GPU。
pub struct Painter { /* 私有:DrawQueue + scale */ }
pub struct DrawQueue { /* 私有:内容队列 / 调试队列 / seq / depth / cur_win / clip */ }
impl Ui {
fn painter(&mut self) -> &mut Painter; // 一个绘制块取一次
fn painter_clipped<R>(&mut self, clip: Option<Rect>, f: impl FnOnce(&mut Painter) -> R) -> R;
fn elem_hint(&self) -> u32; // = painter().elem_hint()
}
impl Painter {
fn new(scale: f32) -> Painter; // 独立构造(单测 / 离屏)
fn commands(&self) -> &[UiDraw]; fn debug_commands(&self) -> &[UiDraw];
fn clip(&self) -> Option<Rect>; fn scale(&self) -> f32; fn elem_hint(&self) -> u32;
fn clipped<R>(&mut self, clip: Option<Rect>, f: impl FnOnce(&mut Painter) -> R) -> R;
// 原语(默认 elem = elem_hint())
fn solid(&mut self, rect, color); fn border(&mut self, rect, color, width);
fn panel(&mut self, rect, bg, border, border_w, radius);
fn panel_elem(&mut self, rect, bg, border, border_w, radius, elem); // elem = 0 容器装饰
fn panel_img(&mut self, rect, bg, img, border, border_w, radius);
fn panel_img_elem(&mut self, rect, bg, img, border, border_w, radius, elem);
fn shadow(&mut self, rect, &ShadowStyle, radius); // elem = 0
fn rounded_at(&mut self, pos, size, radius, color); fn gradient_at(&mut self, pos, size, gradient);
fn icon_at(&mut self, pos, size, icon, color); fn image_at(&mut self, pos, size, bg);
fn text(&mut self, rect, text, size, color, family, align, valign, soft_clip, buf);
fn text_noclip(&mut self, rect, text, size, color, family, align, valign, buf);
fn draw(&mut self, kind: DrawKind, rect, elem); // 底层(显式 elem)
// 调试图元(进 debug 队列)
fn debug_line / debug_rect_outline / debug_circle_outline / debug_cross / debug_grid
}| 约定 | 说明 |
|---|---|
| 一段绘制一个 painter | ui.painter() 借 &mut self;painter 存活期内 ui.text_size / ui.hit_abs / ui.*_at 都借不到 ui。顺序 = 先量 → 画 → 再量 |
elem 默认逐条取 |
原语各自取"录制时的 seq + 1",与旧 Ui::push_* 逐位一致。装饰要压在自家内容之上时重新取一次 ui.painter()(此时 elem_hint 已更大);不要把一个 painter 的 elem 想成冻结值 |
容器装饰用 elem = 0 |
本容器的背景 / 边框 / 阴影:panel_elem(.., 0) / panel_img_elem(.., 0) / shadow(..)。⚠ 语义是"画在本容器元素之下"——只有窗口(win > 0)才天然成立(一扇窗 = 一个排序空间);win=0 的容器靠"顶层放置序 place"获得自己的排序空间(见 ENGINE_GUIDE.md §18.23),所以 drag_panel_at 的底色不会被别的 win=0 内容穿透。滚动条 / 手柄这类"要盖在自家内容之上"的装饰不要传 0,用 elem_hint() |
| 裁剪就在 painter 上 | Clip 沙箱 / ScrollView 可视区 / 严格窗口内容裁剪 = Painter::clip() 的当前层,命令自带它 ⇒ 沙箱里的控件什么都不用做。只有"要一层与当前不同的裁剪"才用 painter.clipped(Some(rect), |p| ..) / ui.painter_clipped(rect, ..)(更窄,或 None 主动不裁),块外自动恢复 |
| 独立可用 | Painter::new(1.0) + commands() ⇒ 无字体图集也能断言"画出了哪几条命令"(rjw_ui 的 painter 单测就是这么写的) |
旧
Ui::push_solid_rect/push_border_rect/push_panel_like(_img)/push_panel_shadow/push_text_rect(_noclip)/rounded_rect_at/gradient_rect_at/icon_at/image_at全部保留,实现为绘制器的薄包装(既有调用点无需改动)。
rjw_ui 只输出批次数据,不直接调用任何渲染器。批次经 [UiBackend] 交给后端:
pub trait UiBackend {
fn texture(&self, uid: u64) -> Option<Arc<TextureWrapped>>;
fn submit(&mut self, batch: UiBatch); // 顺序 = 绘制顺序
}只有两个方法(v0.3):UI 的绘制输出就是「纹理 + 顶点」。后端不需要参与纹理生成 ——矩形渐变已改为四角顶点色(见「渲染增强」一节),不再有程序化渐变纹理请求。
pub type Tri = [u16; 3];
pub struct UiBatch<'a> {
pub texture: Arc<TextureWrapped>,
pub vertices: &'a [VertexP3U2C4], // 已是最终屏幕物理像素坐标
pub indices: &'a [Tri], // 相对 vertices;UI 全程直出三角形
pub transform: Transform2D, // 实例级(窗口 FX 不重建顶点)
pub tint: Color, // 实例级整段染色(顶点色已含控件自身 tint)
pub layer: f64,
pub clip: Option<Rect>, // **批次 scissor**(屏幕物理像素;None = 不裁剪)
pub source: UiBatchSource, // 实例用户数据
}
pub struct UiBatchSource { pub window: u32, pub elements: u32, pub debug: bool }⚠ 借用(v0.3 / 阶段 9):
vertices/indices是借来的切片——几何住在rjw_ui的提交计划缓存里,提交期只借用一次,不再"克隆一份再交给后端"(那是finish里最大的一笔 memcpy)。后端必须在submit调用内消费完(拷进自己的缓冲 / 命令队列),不要存起来跨帧用。RecordingBackend因此把批次存成拥有顶点的RecordedBatch(batches: Vec<RecordedBatch>)。
clip(v0.3):UI 的环境裁剪(严格窗口内容 / ScrollView 可视区 / Clip 沙箱 / 文本框盒)以批次 scissor 交付,几何保持原形(圆角 / 环带 / 投影不再被切平)。 后端把它交给渲染器的命令级 scissor(Draw2D::scissor,见 §5.6);None= 只受 画面级Render2D::scissor约束;空矩形应跳过该批次(不是"不裁剪")。
RecordingBackend提供clipped_batches()/clips()供断言。
indices允许为空——后端此时应按「每 4 顶点一组、顺序TL,TR,BL,BR」的旧四边形 约定补出索引(Render2dUiBackend即如此回退),使外部后端仍可只产出顶点。rjw_ui自己的产出恒带索引:圆角 / 羽化本身就是三角形,四边形只是它的退化情形。
实现者:rjw_krusie::runtime::layers::ui_backend::Render2dUiBackend(桥接到 Render2D);
rjw_ui::RecordingBackend(纯 CPU,收集批次供测试断言 draw call 数)。
实例粒度 / DrawCall 取舍(一个 UiBatch = 一个实例 = 一次 draw call 候选):
| 规则 | 原因 |
|---|---|
| 同一提交单元(窗口 / win=0 顶层放置)内所有控件 / 容器合成一批 | 「尽量减少 DrawCall」——实例内容范围 = 整个单元 |
| 按窗口切 | 批次的 transform / tint 是窗口级的;烘进顶点会让 FX 动画每帧重建整窗顶点,摧毁窗口顶点缓存 |
| 按 win=0 顶层放置切(v0.3 / 阶段 8–9) | 每个放置要有自己的排序空间(否则 elem = 0 的装饰被邻居穿透);同时使"合并结果"能作为计划整体跨帧缓存 |
| 按纹理切 | 一次 draw call 只能绑一个纹理(bind group) |
超 MAX_UI_SEG_VERTS 切 |
u16 索引上限 |
⚠ 批次数 = draw 数:每个
mesh_indexed(..)带自己的矩阵下标,Render2D的动态段 合批对 UI 批次不生效(实测 UI 层draw_op_count() == segs)。所以"分成几个批次" 是真的多画几次——阶段 9 用 +4 次 draw(40 → 44) 换掉了每帧 ~300µs 的顶点拷贝, 详见UI_ARCHITECTURE.md§6.2。
source.elements 记录本批次覆盖的控件数,是上述取舍的可观测指标。
切段规则由纯函数 segment_runs 裁决,契约由 ui::batch_contract_tests 断言
(单窗口单纹理 = 1 次 draw call;控件数不增加 draw call;窗口/纹理切换必切段)。
容器责任链 builder(唯一入口):选项链式设置、.show(f) 终结;
裸布尔全部换枚举(Level / Placement / Resize / Child,见下表),
旧的 6 个 window_at* / 3 个 modal_at* 变体已删除(v0.3)。
枚举选项(取代裸布尔 / 开关方法)
| 枚举 | 变体 | 取代 |
|---|---|---|
Level |
Topmost(默认,点击置顶) / Normal(点击不改 z 序) |
.topmost(bool) |
Placement |
Expand(默认,内容撑高、不裁剪) / Clip(强制裁剪到窗口矩形) |
.strict() |
Resize |
None(默认) / Horizontal(只调宽) / Both(宽高同调) |
show_handle: bool |
Child |
Expand / Fit(子项尺寸策略,用于 child_rect) |
裸 bool 语义 |
单位(Size / Position):坐标 / 尺寸参数
(pos、width、builder 的 .pos/.width 等)接受带单位包装——Size::Logical /
Position::Logical(默认,From<f32/Vec2>,× scale 换算并取整)或 Size::Physical /
Position::Physical(原样)。换算仅在 API 边界一次完成,Ui 内部布局 / 命中 / 绘制
一律物理像素(Ui::scale() 只供边界换算;scale_factor 在 build() 时把 Theme
预乘——全部样式尺寸 / 字号 × scale 取整,见 Theme::scaled)。
label_wrap_at / view_at / scroll_at / list_at / flex_at / grid_at / divider_at /
rounded_rect_at / gradient_rect_at 同样带单位(pos → Position,max_w / size /
view_size / total_h / w / radius → Size)。
实现者规则(写控件 / 扩展 API 的人必读):
From<f32>/From<Vec2>(⇒Logical) 是给调用点的糖。任何 API 函数(内置或自定义)的函数体内部必须显式使用Logical/Physical——解释用to_physical(scale)或match(紧邻参数解包), 造值写Size::Logical(..)/Size::Physical(..)(主题值 / 持久化值一律Physical); 只有 builder setter 的"原样转发"允许隐式。正典见Size的「单位纪律」rustdoc 与docs/UI_ARCHITECTURE.md§5.0。
| 入口 | 链 | 语义 |
|---|---|---|
ui.window(id) |
.pos(..)(不调 = 引擎自动级联,Win32 CW_USEDEFAULT 语义:按首次出现顺序右下偏移 28 逻辑像素,越界回绕;结果持久于 UiState::auto_pos,用户拖拽优先) .width(w) .height(h) .gap(Size) .level(Level) .placement(Placement) .style(PanelStyle) .clamp(WindowClamp) .resize(bool) .title(&str) .close_button(&mut bool) .collapsible(bool, Option<&mut bool>) .show(|w| ..) |
可重叠窗口(唯一入口):点击置顶(焦点 z-order,UiState.window_z)+ 可拖拽(位置持久于 UiState.panel_pos)。设计理念「无顾虑地使用」(docs/UI_ARCHITECTURE.md §0):什么都不写也必须是对的 —— 位置自动级联、宽高由内容撑开、不裁切、无装饰;只有想控制时才写 .width/.height/.vscroll/.resize。尺寸的所有权链单向:内容 → 显式 .width/.height(初始值)→ 用户拖过(持久值接管)。.width = 固定宽(右下角可缩放);.height = 固定高(长内容窗口的有界视口:.width(320.0).height(420.0).vscroll(true) ⇒ 视口 420 逻辑高 + 窗口内滚动条;不调 = 由内容决定);.gap = 内容子项行距(不调 = Theme::gap;下拉 / 菜单浮层用 Physical(popup_gap(scale)) 拿"逻辑 1px"的紧行距);.placement(Clip) = 强制裁剪;.style = 逐窗口样式覆盖(默认 Theme::panel);.clamp = 位置约束(Screen 限位不跑出屏幕(默认)/ Free 自由 / Locked 锁定位置不可拖)。窗口内同一 layer 按"背景/图形→文字"绘制。单轴溢出策略 .vscroll(mode) / .hscroll(mode):入参是 impl ScrollParam —— bool(true = 视口 + 滚动条 / false = 不裁)或三态 ScrollMode::{NoClip, ClipOnly, Scroll}(低层逃生舱:只裁不滚只能写 ClipOnly): |
NoClip(默认):该轴压缩内容 ——.width(..)/ 拖出来的尺寸是固定宽,子项被压进可用宽(row里最后一个控件按余量缩;Label自动换行 / 省略号);ClipOnly:该轴裁切内容(视口:子项按自然宽排、超出被裁、无滚动条);Scroll:视口 + 滚动条(滚轮 / 拖 thumb / 点轨道翻页)。两条轴都真的实现了:vscroll(true)= 右侧竖条、hscroll(true)= 底部横条(两条轴同时溢出时拐角互让)。- ⚠
hscroll(true)(=Scroll)的那条轴不折行:沙箱不再上报可用宽 ⇒ 内容保持自然宽、超出横向滚(这是"能横滚"的前提)。要按窗口宽折行就用hscroll(false)(默认)。 - 显式设置覆盖
.placement(..);没给时老语义不变(Placement::Clip⇒ 两轴都裁; 被拖过尺寸的那条轴自动成为视口)。 - 两条轴互相独立(旧的
Placement::Clip做不到):只裁横向的窗口,纵向仍能由内容撑高。
拖拽缩放(egui 风) .resize(allow: bool):只给"能不能拖大小"(轴不可显式指定),允许的轴自己推导、水平轴"压缩还是横滚"由 hscroll 决定:
垂直轴有视口?(.vscroll(非 NoClip) / 给了 .height(..) / .placement(Clip)) |
.resize(true) 允许的轴 |
水平轴内容 |
|---|---|---|
| 是 | 垂直 + 水平都能拖 | hscroll(true) ⇒ 横滚(自然宽 + 横条);否则按宽压缩 |
| 否(默认,高度由内容定) | 只有水平能拖 | 同上 |
.resize(false) = 不画柄也不响应拖拽。不调 .resize(..) = 旧行为(有 .width(..) 就能横向拖)。收起态整条缩放链路关闭(没有尺寸可调:收起就是一行标题栏,柄的命中区会压住 ⌃/✕,按下种子还会把"收起后的那一行高"写成持久高 ⇒ 展开回来是一条缝)。
Resize 枚举(控件级):Resize::{None, Horizontal, Vertical, Both}(↔ / ↕ / ↖↘)现在只服务控件级缩放(TextEditor::resize(..) / resizable_text_*_at);窗口层已无显式轴向入口(需要"只可调高"就给窗口一个纵向视口:.height(..) / .vscroll(true))。窗口柄与内容里的缩放柄 / 标题栏按钮重叠时内容与按钮优先(柄的应用推迟到内容之后且只在没人认领按下时生效)。
持久尺寸优先(两条轴同口径):.width(..) / .height(..) 只是初始值,用户拖过之后由 UiState::{window_widths, window_heights} 接管(persisted.or(explicit))。⚠ 让显式值压过持久值会出现"第二次拖柄时那条轴弹回原位"(用户实测:"拖拽缩放柄到别的地方,然后下一次点击 x 坐标弹回"——eg260818UI 里只有唯一没有 .width() 的 strict_win 不弹,正是反证);--sim-resize[第二次拖柄不弹宽] 是它的回归守卫。外框(标题栏 / × / 收起)见下 |
| ui.panel() | .pos(..) .drag(id) .style(..) .show(\|pp\| ..) | 面板 = panel_at + drag_panel_at 统一入口 |
| ui.modal(id) | .pos(..) .width(w) .show(\|m\| ..) | 模态对话框(唯一入口) |
菜单栏(横向触发器 + 同一套下拉面板):
| 入口 | 链 | 语义 |
|---|---|---|
ui.menu_bar(id, pos, |bar| ..) |
bar.menu(label, |m| ..) → MenuCtx::{item, item_checked, submenu, caption, separator};bar 本身就是一行:bar.add(..) / bar.button(..) / bar.label(..) / bar.text_input(..) / bar.divider() / bar.separator_v()(竖向分割线)/ bar.width(..) / bar.bg(..) / bar.border(..) / bar.border_w(..) / bar.radius(..) |
本质 = 一行(row)+ 一条覆盖整栏宽度的背景。返回栏尺寸;pos = 栏左上角(顶层 = 屏幕坐标),栏不占父容器光标。bar.width(Size::Logical/Physical) = 背景铺多宽(不调 = 子项撑多大就多大,如整条屏幕宽;只影响背景,不 clamp 子项)。MenuBar Deref 到 Pack ⇒ UiAdd 的方法(add / label / button / text_input / divider / min_size …)直接可用 ⇒ 栏里能塞任何控件(含 Divider::vertical() 竖向分割线);右推某块用 bar.min_size(剩余宽, 0.0); bar.label("")。样式取 Theme::menubar(独立于 Theme::button):栏底 = bg + 只画底边的 border/border_w、栏内边距 padding、子项间距 gap;触发器常态 trigger_bg 默认全透明(纯文字菜单条,不是一排按钮)、悬停 / 展开用 trigger_hover / trigger_pressed 圆角高亮、宽 = 文字宽 + 2 × trigger_pad_x;separator_v() 用 separator/separator_w/separator_margin。在子项之后以 elem = 0 录 ⇒ 压在同深度底层之上、所有控件之下(⚠ 不盖在普通窗口之上)。展开状态跨帧持久于 UiState::menu_open(触发器绝对 ID);同一时刻只有一个菜单开着,点菜单项 / 点栏外 / Esc 都收起(点另一个触发器 = 切换,不算点外;点栏内空白 / 竖分割线 / 栏里的别的控件也不算点外 —— 纯函数 widgets::menubar::menu_bar_should_close 逐组合单测)。MenuCtx Deref 到 Window ⇒ 菜单里同样能放 label / button / divider / row(横向排版)/ text_input / add(..),以及 Item 责任链菜单项(含 Submenu:Hover 在行右侧展开)。下拉面板与 Dropdown 共用同一实现(crate::widgets::menu::popup_show):Level::Normal + WindowClamp::Locked(点它不置顶、拖不动)+ 不画缩放柄 + z 强制 WIN_TOPMOST 哨兵 + 行距 popup_gap(scale) —— 所以菜单栏录在哪里都盖得住别人。⚠ 面板是嵌套窗口(录在栏容器里)⇒ debug_dump() 的 origin 相对栏,算屏幕坐标要叠加栏原点。面板排版由引擎保证:内边距 = ComboStyle::item_pad_x(菜单项 / caption / separator 天然同列)、面板宽取上一帧结算宽(子项高亮 / 分割线铺满面板)、勾选是方框且画在项内容里、caption 用 text_muted + 小字号做分组标题。细节见 docs/ENGINE_GUIDE.md §18.13/§18.15;几何可 RJ_MENU_TRACE=1 打印(栏矩形 + 收起判定事实) |
窗口外框(标题栏 / 关闭 / 收起):三个选项各自独立、都不调就完全没有外框
(逐像素等于旧行为);任一开启都在窗口内容第一行录一条标题栏(底色
Palette::surface_raised + 面板同色边框 ⇒ 通条,底边那条就是分隔线)。
| 选项 | 签名 | 语义 |
|---|---|---|
.title(t) |
title(text: &str) -> Self |
标题文字(过长按省略号截断,不撑宽窗口);标题栏空白处仍可拖动窗口。标题行贴窗口顶边(条高 = title_bar_h(row_h) = 一行,纯函数):下面的内容与窗口高度各少一个 pad_total(用户实测"可以往上抬");条只是背景装饰、不裁剪内容 ⇒ 标题 / ▲ / ✕(边长 row_h - 2)允许比条高 |
.close_button(open) |
close_button(open: &mut bool) -> Self |
标题栏右侧画 ×;点击把 *open 置 false。*open == false 时整窗短路——不录制、不写原点/尺寸、不占遮挡矩形(不会留下"看不见却挡点击"的窗口);重新打开是应用的责任(把 *open 置回 true,如菜单勾选) |
.collapsible(show, collapsed) |
collapsible(show: bool, collapsed: Option<&mut bool>) -> Self |
show = 是否画 ⌃ 按钮;收起状态两种所有权:Some(&mut bool) = 应用持有(点 ⌃ 取反;show = false 时按钮不画、但 *c 照旧生效 ⇒ 可由菜单 / 代码收起展开),None = 引擎托管(状态存 UiState::collapsed,按窗口绝对 ID;点 ⌃ 由引擎翻转,应用用 UiState::{is_collapsed, set_collapsed, toggle_collapsed} 读 / 改,UiState::reset() 一并清空)。两种语义都是点击当帧不变、下一帧生效 |
*collapsed 在录制开头读取(点击当帧不变、下一帧生效);× / ⌃ 上的按下会
认领(claim_press)⇒ 点按钮不会顺带拖动窗口。
caption 按钮的位置(Windows 风格):[⌃][✕] 顺序(✕ 恒在最右)用
Ui::add_at 绝对定位在窗口外框坐标系——固定尺寸窗口下簇右缘 = 外框右缘 −
TITLE_BUTTON_INSET(0)、y = 0、高 row_h;最右按钮的右上角取面板右上圆角,
贴外缘时不会戳出圆角。自动宽窗口(不调 .width(..))没有"外框右缘"可贴 ⇒ 簇跟随标题。
落点由纯函数 ui/chrome.rs::title_bar_layout 解算(单测钉住"贴右缘")。⚠ 旧版把它当行内子项、
用 spacer = 内容宽 − 标题宽 推到内容右缘 ⇒ 离窗口右缘永远差 pad + 4(实测 18px)。
选项载体 WindowOptions / PanelOptions(公开,可独立构造/复用)。容器闭包内经
UiAdd::window(id) / UiAdd::panel() 同样可用。
| 函数 | 签名 | 语义 |
|---|---|---|
label_at |
ui.label_at(pos, text) -> Vec2 |
place:绝对定位 + 内容自然尺寸 |
pack_at |
ui.pack_at(pos, side, |p| ...) -> Vec2 |
pack:按 PackSide::Top/Left 堆叠,宽/高 = 最大子项 |
panel_at |
ui.panel_at(pos, |pp| ...) -> Vec2 |
背景 + 边框 + 内容垂直堆叠,尺寸自动包裹(等价 ui.panel().pos(pos).show(..)) |
drag_panel_at |
ui.drag_panel_at(id, pos, |pp| ...) -> Vec2 |
同 panel_at,且按住面板任意处可拖动(位置持久于 UiState.panel_pos;拖动期间子控件不响应;等价 ui.panel().pos(pos).drag(id).show(..)) |
scroll_at |
ui.scroll_at(pos, view_size, id, |s| ...) -> Vec2 |
滚动容器(默认:纵向滚动 + 横向裁切):内容在可视区内垂直堆叠(pack Top),滚轮 / 滚动条(拖 thumb、点轨道翻页)滚动;可视区外裁剪;偏移持久于 UiState.scrolls(offset / offset_x) |
scroll_axes_at |
ui.scroll_axes_at(pos, view_size, id, v, h, |s| ...) -> ScrollOutcome |
按轴的滚动容器:v / h 各传 impl ScrollParam(bool 或 ScrollMode);Scroll 的那条轴画滚动条 + 吃滚轮,也是该轴不折行的前提(hscroll(true) ⇒ 内容自然宽) |
scroll_area |
ui.scroll_area(id, view_size).pos(..).vscroll(bool).hscroll(bool).show(|s| ...) -> ScrollOutcome |
滚动容器 builder([ScrollArea],widgets/ 里):scroll_axes_at 的责任链写法,返回 ScrollOutcome { view, content }。⚠ 滚动沙箱不 note 内容(视口语义)⇒ 顶层用显式 pos / size,或放进有界容器 |
grid_at |
ui.grid_at(pos, cols, id, |g| ...) -> Vec2 |
均匀网格;id 缓存单元格尺寸(跨帧稳定) |
flex_at |
ui.flex_at(pos, total_h, &[w1,w2,..], |f, i| ...) -> Vec2 |
flex 容器:固定总高 total_h 按 weights 权重等分子项高度(扣 gap;回调按索引布局,同帧精确);内容超高溢出可见(需滚动时内嵌 scroll_at) |
namespace |
ui.namespace(id, |ui| ...) -> Vec2 |
ID 命名空间区块:正文录在当前光标处、只给内部控件加 id/ 前缀(Ui::id_for 的绝对 ID 因此带上它)⇒ 同名控件互不干扰。不做任何布局 / 绘制(无背景 / 内边距 / 裁剪,也不另开 frame)——与"不用它"逐像素相同;正文是当前容器的子项(avail_w / gap 照旧),父光标由正文各项自己推进;返回正文结算尺寸(空内容 = (0,0))。嵌套顺序拼接:"a" 里的 "b" 里的 "kw" ⇒ "a/b/kw"。窗口 / 面板 / 滚动容器 / grid / 区块本身已是命名空间边界,本入口给"只想要 ID 隔离、不要容器"的场合(闭包参数是 [PackEntry],实现了 UiAdd) |
foldable |
ui.foldable(id, label) -> Foldable → .open(bool) / .closed() / .show(|ui| ...) -> FoldState |
可收缩区块:标题行(一整行,高 Theme::row_h、宽铺满容器内容宽:▶ / ▼ + 文本;样式 Theme::foldable)+ 可折叠正文。默认折叠(首次只见标题行);.open(true) 改首次展开(只影响从未被点过的区块——首帧把默认态落盘到 UiState::folded,之后由那张表说了算)。点标题行即翻转(按下边沿 + 命中,或键盘 Enter / Space;当帧几何不变、下一帧生效 —— 与窗口 ⌃ 同口径。⚠ 判据不含 Response::clicked:它在释放帧成立,与按下边沿叠加会让一次点击翻两次);Sense::DRAG ⇒ 按下被认领(不会顺带拖动外层窗口 / 面板)。折叠 = 正文完全不录制(不占高、不参与布局、不进命中表、不产生顶点;正文内部跨帧状态不清,展开回来还是原样)。正文录在本区块的 ID 命名空间里 ⇒ 两个区块里的同名控件互不干扰;正文整体按 FoldableStyle::body_indent 缩进(光标 + 内容最大宽一起右移 ⇒ 绘制 / 命中天然一致,容器尺寸不变)。返回 FoldState { header: Response, folded: bool, header_rect: Rect, body_h: f32 }(folded 是录制开头读到的值) |
foldable_custom |
ui.foldable_custom(id, |t| ...) -> Foldable(容器内)/ Foldable::custom(ui, id, |t| ...)(裸 Ui) |
自定义标题内容的区块(标题即标准容器):闭包在"固定宽(= 标题文本区宽,pad_x + icon_w 之后)/ 高度自然"的装饰容器(ornament_entry_natural_h,不 note_content)里跑 ⇒ 标题内容绝不反过来撑大容器,但自己长高(多行 / Row ⇒ 标题块按内容加高,正文随之让位:标题行矩形加高 + 父容器光标补推同样多)。里面可放任意控件(label / row / checkbox_mut / button / add …),控件 id 仍在本区块命名空间里(id/ 前缀),且它们自己认领按下 ⇒ 点它们不会连带折叠标题。Foldable::custom(ui, ..) 的首参是裸 Ui(不在容器闭包里的调用点,如 Frame::ui(..) 给的 &mut UiSession 经 Deref);容器闭包里用 UiAdd::foldable_custom。⚠ 命中区仍是首行(interact 发生在内容之前、此时高度还未知) |
容器内 *_at(offset) |
p.panel_at(offset, |inner| ...) |
嵌套容器(相对当前容器内容原点,不占光标) |
Widget 只有一个方法(尺寸在 ui() 里就地申请,参考 egui):
pub trait Widget {
fn ui(self, ui: &mut Ui) -> Response; // 申请 → 画 → 收交互 → 返回响应
}
impl Ui {
// 申请(尺寸 = 物理像素 Vec2;要逻辑单位先 to_physical(scale))
fn allocate(&mut self, size: Vec2) -> Rect;
fn allocate_mode(&mut self, size: Vec2, mode: Expansion) -> Rect;
fn allocate_at(&mut self, pos: impl Into<Position>, size: Vec2) -> Rect;
fn allocate_sense(&mut self, id: &str, size: Vec2, sense: Sense) -> (Rect, Response);
fn allocate_sense_mode(&mut self, id: &str, size: Vec2, mode: Expansion, sense: Sense) -> (Rect, Response);
fn allocate_sense_at(&mut self, pos: impl Into<Position>, id: &str, size: Vec2, sense: Sense) -> (Rect, Response);
// 交互(申请与交互分开时用;`allocate_sense*` 已含这一步)
fn interact(&mut self, id: &IdAbsolute<'_>, rect: Rect, sense: Sense) -> Response;
// 显式 rect 的控件(`*_at` 老 API)与自定义控件:绝对放置也要算进容器尺寸
fn note_placed(&mut self, rect: Rect);
// 可拖拽缩放控件:把跨帧持久尺寸(责任链 → 用户拖拽值)并进本帧申请尺寸
fn resolved_size(&mut self, id: &str, fallback: Vec2) -> Vec2;
}
pub struct Sense { pub hover: bool, pub click: bool, pub drag: bool, pub focus: Option<FocusKind> }
impl Sense {
pub const NONE / HOVER / CLICK / DRAG: Self;
pub fn focus(self, kind: FocusKind) -> Self; // 进焦点链(Tab + 焦点描边)
}Sense::drag= 按下即claim_press()(外层窗口 / 面板不把这次按下当作拖动基准)update_drag;拖拽基准与数值映射仍由控件维护(WidgetState::{press_mouse,press_panel})。
interact一次做完:命中(含窗口 / 控件级遮挡 / 裁剪过滤)→ 焦点 → 按下认领 →update_interact(hover / pressed / clicked / released)→note_press_handled→update_drag。Response增rect: Rect(本帧最终矩形;UiAdd::label就靠它返回尺寸)与culled: bool(被裁剪层完全剔除 ⇒ 控件应立刻return resp,不镶嵌不入段);pressed= 持续按住(与ButtonState::pressed同义),clicked= 本帧完成点击。Ui::culled(rect) -> bool:只用allocate(只要矩形)的控件自己判一次可见性, 等价于Response::culled。scissor 只省片元,剔除才省镶嵌/顶点/draw—— 见docs/ENGINE_GUIDE.md§18.22。Ui::note_placed(rect):显式 rect 的控件(button_at/radio_at/text_input_at…)录完要点一次,让"画在容器外"这件事要么让容器长大、要么被Clip裁掉;自定义控件若自己算矩形(不经allocate*)也必须点一次。Ui::resolved_size(id, fallback):可拖拽缩放控件的"当前尺寸",与Ui::resize_handle配对(前者读、后者写)。要在ui()里申请之前调用它, 否则会出现"画的是拖大的框、申请的却是默认尺寸"——容器不长、后续控件不动、 框溢出父级(TextEditor踩过;正确写法见该方法的 rustdoc 示例)。- 膨胀语义是申请方式:
Expansion::{UnlimitedExpansion(默认), LimitedInParent(压到 avail_w), DisableAutoExpansion(不撑大父级)}; min/max 用apply_constraints(desired, c)自己应用。 Ui::avail_w():由内向外找第一个给出宽度约束的容器(固定宽窗口 / 从父级继承的内容最大宽 / 沙箱宽),并与"下一子项max_size"和水平行的剩余宽取最小。⇒LimitedInParent控件 在row里也拿得到外层窗口的可用宽(自动换行 / 压窄),嵌套容器不会把内容排到固定宽窗口外面; 同时,可拖拽缩放的控件要自己把申请尺寸与下限都压到avail_w/ 实际申请宽以内 (TextEditor是范例:.resize(..)的下限不压住就会被主题下限撑回去)。add_at(pos, w):给Ui打一次性放置覆盖,控件的第一次申请消费它(只在第一次; 控件要摆多块用allocate_at)。- ⚠ 顺序:先量(
text_size/avail_w)→ 再申请 → 再画(ui.painter(),一个绘制块一个)。
| 控件 | 签名 | 返回值 / 行为 |
|---|---|---|
label |
p.label(text) -> Vec2 |
文本,内容自然尺寸 |
label_ex |
p.label_ex(text) -> LabelEx(裸 Ui 用 ui.label_ex(..) / Label::ex(text)) |
扩展标签(高度自定义文本):整组 TextStyle(.style(..))+ 字段级糖(.font_size / .font_family / .weight / .italic / .stretch / .letter_spacing / .line_height / .align / .valign / .wrap / .ellipsis / .tint)+ 首末两色渐变(.gradient / .gradient_v / .gradient_axis + 域 `.gradient_mode(Glyph |
colored_label |
p.colored_label(text, color) -> Response |
彩色标签糖:label_ex(text).tint(color) 一步到位(返回值 Response,rect = 占用矩形)。等价的老写法是 p.label(text)(只要尺寸、跟随主题色) |
label_wrap |
p.label_wrap(max_w, text) -> Vec2 |
自动换行标签:max_w(逻辑像素)内按词/字换行;宽 = min(自然宽, max_w),高 = 行数 × 行高;max_w <= 0 = 不换行 |
min_size |
p.min_size(w, h) |
下一子项最小尺寸约束(0 = 该轴不约束;一次性,作用于紧接着的下一个子项) |
max_size |
p.max_size(w, h) |
下一子项最大尺寸约束(同上) |
row |
p.row(|r| ..) -> Vec2 |
水平行(占光标):子项左上角对齐、沿 X 推进;单行子项(SizeClass::SingleLine,默认)被钉到行的标准高(默认 Theme::row_h),多行子项(SizeClass::Multiline,如多行 TextEditor)以它为下限、可撑高整行 |
row_builder |
p.row_builder().min_h(..).max_h(..).height(..).gap(..).pad(..).wrap_w(..).wrap().line_gap(..).show(|r| ..) -> Vec2 |
行的可配置形态(crates/rjw_ui 的 RowBuilder):min_h = 行高下限且是单行子项的标准高(默认 Theme::row_h)、max_h = 上限(超出的子项照录,溢出可见)、height = 固定行高;尺寸收 Size<f32>(逻辑默认)。min > max 时 min 胜。自动换行:wrap_w(w) = 行宽上限(与父级可用宽取 min)、wrap() = 用父级可用宽、line_gap(g) = 折行的行间距(默认 = gap)—— 折行只换行不压缩(不再报行内剩余宽)、行内左上角对齐、空行不折;不调 wrap* 则行为与旧版一字不变 |
row_wrap |
p.row_wrap(max_w, |r| ..) -> Vec2(裸 Ui:ui.row_wrap(..)) |
自动换行的水平行(占光标):max_w = 行宽上限,塞不下就收行。等价 row_builder().wrap_w(max_w).show(f)。RJ_ROW_TRACE=1 打印宽度来源与结算尺寸 |
SizeClass |
Widget::size_class() -> SizeClass |
控件在行里被怎么钉高:SingleLine(默认,钉到标准行高)/ Multiline(标准行高只是下限)。自定义控件想被行撑高就覆写(不覆写 = 旧行为) |
button |
p.button(id, label) -> ButtonState |
hover / pressed / clicked(按下+释放均在本体) |
slider |
p.slider(id, range, value) -> f32 |
拖拽;返回更新后的值(越界 clamp) |
Slider(builder,泛型) |
p.add(Slider::new(id, range, &mut v).drag_sensitivity(..).shift_speed(..).ctrl_speed(..)) |
滑块:range 与 &mut v 的类型决定 T: SliderValue(f32 / f64 / 全部整数类型)。内部一律 f32 数学(与渲染 / 命中同单位),进出各换算一次;整数类型在回写时四舍五入 ⇒ 范围是整数时"一格一格跳"、f64 只是接口便利(滑条分辨率本来就受物理像素限制)。⚠ 没有 step 吸附:拖出来的是连续值(想网格化就绑整数类型,或与一根 NumberInput 配对) |
checkbox |
p.checkbox(id, label, checked) -> CheckboxState |
.toggled() 本帧切换;checked 由用户维护 |
radio |
p.radio(id, group, label) -> CheckboxState |
组内互斥(UiState.radio_groups);.checked() 读选中 |
text_input |
p.text_input(id, &mut String) |
单行输入框:点击聚焦/定位光标、打字/退格/删除/方向键、Enter/Esc 失焦、光标闪烁;超长文本滚动跟随光标(光标始终可见)、拖选文本 + Ctrl+C/V/X 复制/粘贴/剪切(选择优先于窗口拖拽);支持中文 IME(组合候选浮动提示框 + 候选框定位到光标) |
text_area |
p.text_area(id, &mut String) / p.text_area_at(id, rect, &mut String) |
多行文本输入框:Enter 换行、↑/↓ 跨行(保持列)、Home/End 行首尾、按宽度自动换行、超出高度垂直滚动(滚轮 + 光标跟随)、跨行选择 + Ctrl+C/V/X、IME 支持;光标按逻辑行(\n)定位(超宽长行换行后近似)。垂直对齐默认 TextVAlignMode::TopLeft(文字与光标一起从框顶下垫 InputStyle::padding_y ⇒ 框被拉高时位置不变;要用居中写 TextEditor::new(..).valign(TextVAlignMode::CenterLeft),光标 / 选择 / 点击行号一起走) |
TextVAlignMode |
TextEditor::valign(TextVAlignMode) |
多行编辑的垂直对齐:TopLeft(默认,顶对齐 + padding_y 垫高;与单行输入框同宽同高时视觉一致)/ CenterLeft(内容装得下时居中,装不下退回顶对齐 + 可滚动)。⚠ 光标永远与文字共用同一个偏移 |
NumberInput |
p.add(NumberInput::new(id, &mut v).range(min, max).step(s)) |
数字条:数值类型 T: SliderValue 泛型(f32 默认 / f64 / 全部整数类型,由 &mut T 推断)——浮点默认步进 0.01(每物理像素 ±0.01、显示 2 位小数)、整数类型默认步进 1、显示无小数、输入只收整数文本、拖到类型边界饱和;要更细就显式 .step(0.001)(3 位)。右侧 GRIP_W(公开常量 20px)宽那条手柄水平拖动调值(向右 = 增;Shift ×10 / Ctrl ×0.1;拖到窗口边缘自动 warp),文本框点击 = 进入编辑(浮点收数字 / 负号 / 小数点 / 空格;整数连小数点一起过滤)。精度四条:① 内部数学用 f64;② 拖动吸附到 step 格点后按 step 的十进制位数取整(0.1 → 1 位 ⇒ 存的是"最邻近 0.1"的 double,应用侧 v == 0.1 成立;旧实现存 0.30000001…);③ 先吸附再 clamp(旧顺序会顶出 max);④ 拖动吸附、打字不吸附(step 是拖动精度;手打值只 clamp,并如实显示到能表示它的小数位,不糊成 step 的位数)。常见组合:滑杆后跟数字条(拖滑杆粗调、数字条精确输入,两者绑同一个 &mut T)——eg260818UI 的主题调节窗口整列都是这个形态,脚本化验证见 --sim-tuner(断言 radius == 18.0 精确相等) |
Dropdown |
p.add(Dropdown::options(id, label, &mut u32, &[&str])) / p.add(Dropdown::new(id, label).menu(|m| ..)) |
按钮下拉菜单(下拉框与菜单栏下拉简并后的唯一入口;Widget ⇒ 任意容器 add):options = 选项列表模式(菜单项由引擎排:选中行打勾 + 整行高亮,点击写回 &mut u32 并收起,键盘 ↑/↓ 切换);menu(..) = 富内容模式,闭包参数是 MenuCtx(Deref 到 Window)⇒ 菜单内又可以 UiAdd::add(文本输入 / 分割线 / 菜单项 / 横向排版 / 子菜单)。.side(PopupSide::Below|Right)(默认下方 2px)/ .width(..) / .font_size(..)。展开状态 = UiState::combo_open()(控件绝对 ID);点触发器切换、点项执行+收起、点面板外 / Esc 收起、点任意 WIN_TOPMOST 浮层不收起(子菜单用)。面板 = 锁定位置 + 不画缩放柄 + WIN_TOPMOST。几何助手(公开,脚本算坐标用):item_h(font_size) / popup_padding(theme) / popup_origin(trigger, side) / popup_gap(scale)(行距 = 逻辑 1px 的 floor 值,见 MENU_GAP)。见 docs/ENGINE_GUIDE.md §18.15,仿真 --sim-dropdown(7 段) |
Item(菜单项责任链) |
m.item(Item::new("…").click_behavior(MenuClick::Keep)) / .checked(&mut bool) / .submenu(|s| ..) |
菜单项 builder(MenuCtx::item 接受 &str(旧糖,From<&str>)或 Item;返回"本帧是否被点击")。.click_behavior(MenuClick::{Close,Keep}) = 点击行为 flag(点完收起 / 保留 popup);.checked(&mut bool) = 勾选项(方框 + 点击自动翻转);.submenu(|s| ..) = 子菜单(Submenu):普通项样式 + 右侧 ▸,Hover 在行右侧展开(PopupSide::Right),点击不收起,子面板里点项 ⇒ 整条链收起;状态是行自持的 WidgetState::submenu_open(不碰 combo_open)。便捷糖:m.item_checked(label, &mut bool) / m.submenu(label, |s| ..) |
combo / combo_at |
p.combo(id, current, &[String], Option<u32>) -> Option<u32> |
旧入口(糖):= Dropdown 的选项列表模式 + 固定布局宽,签名 / 返回值 / 行为与旧版一致(None = 无选择 / 未展开;Some(i) = 本帧新选中)。新代码请用 Dropdown;FontModal 的字重下拉仍走它(老代码不必改) |
Segmented |
p.add(Segmented::new(id, &["紧凑","标准","宽松"], &mut idx)) |
分段按钮组(互斥选项拼在一起):相邻段共享边、只有整组外侧角是圆角、选中段高亮;点击把新索引写进 &mut usize。段间分隔线与 ButtonStyle::border_w 解耦(边框关掉时退化成 Palette::surface_dim,否则三段连成一条)。.font_size(..) 可覆盖字号。见 docs/ENGINE_GUIDE.md §18.14 |
| 类型 | 方法 | 说明 |
|---|---|---|
ButtonState |
hovered()/pressed()/clicked()/released() |
按钮状态(本帧点击 = 按下+释放均在本体) |
CheckboxState |
checked()/toggled()/clicked() |
勾选框 / 单选状态 |
UiState |
is_folded(id) / set_folded(id, folded) / toggle_folded(id) -> bool |
可收缩区块(ui.foldable(..))的折叠状态(id = 区块绝对 ID)。哈希集合语义:只有"当前折叠"这一个事实 ⇒ 无记录 = 展开;set_folded(id, false) = 清除记录、回到"首次渲染的默认态"(不是"记住展开")。点标题当帧只写状态、下一帧才改布局;reset() 一并清空。与窗口的 is_collapsed / set_collapsed / toggle_collapsed(另一张表,语义不同)分开 |
Theme { label, panel, button, slider, input, checkbox, divider, menubar, debug, focus, modal, combo, foldable, gap, row_h, feather, line_spacing, font_weight, palette },子样式见 crates/rjw_ui/src/style.rs:
主题序列化(TOML)(rjw_ui 的 serde / toml feature,默认开):
| API | 语义 |
|---|---|
Theme::to_toml() -> Result<String, String> |
全量导出:format_version 头 + [theme] 字段树。⚠ panel.bg_image 不序列化(纹理 uid 不可移植) |
Theme::from_toml(s) -> Result<Theme, String> |
从 TOML 加载(起点 Theme::default();缺字段回落默认、多余键忽略) |
Theme::apply_toml(&mut self, s) -> Result<(), String> |
在当前主题上合并覆盖(文件里出现的字段才改)——手写 gap = 12 这类小文件的语义 |
THEME_FORMAT_VERSION(rjw_ui::theme_toml) |
格式版本;加载时比本引擎新⇒报错拒载。加字段不用改它,改名 / 删字段要改 |
BUILTIN_THEMES / builtin_theme_toml(name) / builtin_theme_names() |
仓库内置主题:crates/rjw_ui/themes/*.toml 经 include_str! 编译期导入(不读磁盘 ⇒ 测试不依赖运行目录) |
apply_builtin_theme(&mut Theme, name) -> Result<(), String> |
按名字把内置主题合并到当前主题(apply_toml 语义);未知名字的错误消息列出可用名字 |
内置主题是自动化测试的输入(也是"编译期导入 TOML"的落点):单测
builtin_themes_load_and_change_the_theme 逐个解析并核对每个键路径都存在于
Theme 字段树(多余键按向前兼容被静默忽略 ⇒ 字段名写错时"加载成功但什么都没改",
这条把它变成失败),every_theme_file_in_the_repo_is_registered 自己列目录核对
"themes/ 里每个 .toml 都登记了"(漏登记 = 文件躺在仓库里没人跑)。
示例侧 --theme builtin:<名字> 即自动加载(无需路径)。
Weight 存 u16(weight = 700)、Align 存小写名、CornerRadius 反序列化两种写法都收
(radius = 6.0 或 radius = { tl = .. })、Brush(背景刷)用显式
{ kind = "solid|vertical|horizontal", colors = [..] }(不要用 serde 默认的外部标签枚举
{ Vertical = [..] }——那在 TOML 里是数组表,读回来报 wanted exactly 1 element;加载侧兼容旧写法)。
示例侧入口:顶栏「导出主题…」「导入主题…」(rfd)
--theme <路径>(启动载入;与导入同一条通路)+--sim-theme <路径>(脚本化验证)。 见docs/ENGINE_GUIDE.md§18.16。LabelStyle(font_size/color/align)、PanelStyle(bg/border/padding/radius/shadow/grip)、ButtonStyle(三态 bg + padding + radius)、SliderStyle(track/fill/handle)、InputStyle(bg/border_focus/caret/sel_bg/preedit/padding_x/padding_y(多行与单行共用的垂直垫高,默认 3 逻辑像素)/height/min_w + radius + grip:可缩放文本框的柄形状/尺寸/颜色,默认GripShape::Diagonal(三条斜线)、颜色随Palette::text_dim)、CheckboxStyle(box_size/checked_fill/gap)、DividerStyle(color/thickness/margin;容器内水平线的宽度由容器结算宽决定,见ui.divider())、DebugStyle(layout_outline / layout_outline_width)、FocusStyle(color / width,键盘导航焦点描边)、ModalStyle(dim / size)、ComboStyle(下拉浮层现代菜单:menu_bg/border/radius/pad_v + item_hover/selected/pad_x/min_w + fg/fg_mark)、MenubarStyle(菜单栏独立样式组:bg/ 底边线border+border_w/radius(恒通栏直角,不参与with_radius)/padding/gap/font_size+font_family/ 触发器fg+trigger_bg(默认全透明 ⇒ 纯文字菜单条,不是一排按钮)+trigger_hover+trigger_pressed+trigger_radius+trigger_pad_x/ 竖分割线separator+separator_w+separator_margin)。整组替换入口Theme::with_menubar(..);with_border_w/with_font_size/with_font_family/with_radius会级联到它(with_radius只改触发器圆角); ⚠ 手写 TOML 的颜色是 0–1 归一化浮点(写 0–255 整数会被当成 >1 ⇒ 夹到全白)。见docs/ENGINE_GUIDE.md§18.13。FoldableStyle(可收缩区块标题行,Theme::foldable,整组替换Theme::with_foldable(..)):bg(常态,默认全透明——标题不是按钮)/bg_hover/bg_pressed(三态优先级pick_bg:按下 > 悬停 > 常态)/fg(文字)/mark(三角图标,默认Palette::text_muted比文字弱一档)/border+border_w(默认 0 = 不画)/radius(默认 0;with_radius级联取min(r, 6),与菜单触发器同口径)/font_size+font_family/pad_x(左右内边距)/icon_w(三角预留宽,含与文字的间距)/icon_h(三角边长)/ 正文归属提示:body_indent(左缩进)/guide+guide_w+guide_tail(左侧竖引导线)/fade_h+fade(默认关闭;> 0时正文上下缘各一条"阴影色→全透明"的渐变提示)。 两个预设:FoldableStyle::button_like(&palette)(标题行做"类按钮"块:常态底色 + 描边 + 圆角, 行为不变 —— 整行本来就是命中区)、with_body_fade(h, color)/with_body_guide(..)。with_border_w/with_font_size/with_font_family同样级联到它。Theme::default()浅色,Theme::dark()深色。
布局令牌("同一套界面在小屏排得下、在大屏更舒展"):Theme::density(Density) 一趟按比例
缩放间距 + 字号 + 行距(Compact 0.84/0.92/1.10、Cozy 默认 = 1.0/1.0/1.2、
Spacious 1.18/1.08/1.30);单维微调用 with_font_scale / with_spacing_scale /
with_line_spacing(都是倍率、在现值上叠乘)。Theme::line_spacing(行高 = 字号 × 该值,
默认 DEFAULT_LINE_SPACING = 1.2)只作用于可能换行的文本(多行 TextArea / wrap(..)
标签),wrap <= 0 的单行文本行高恒等于字号;Theme::scaled(DPI) 不缩放它(倍率不是尺寸)。
eg260818UI 的「主题调节」窗口可实时切档 + 拖三根倍率滑杆。
字重令牌:Theme::font_weight: Weight(rjw_ui::Weight = fontdb 的 Weight(u16),
常量 THIN 100 … NORMAL 400 … BLACK 900;默认 NORMAL = 与扩展前逐像素一致),
入口 Theme::with_font_weight(w)。它是全局文本令牌:作用于 Ui 里所有排版
(标签 / 按钮 / 输入框 / 下拉 / 换行文本……——它们都经同一对出口建缓冲)。
字体没有该字面时由 cosmic-text 按最接近的字面回落。不是尺寸量:
Theme::scaled(DPI) / Density 都不碰它。⚠ 字重会改字形与步进宽度(布局随之变)
⇒ 排版缓冲缓存键与窗口 / win=0 子槽的几何签名都含字重,改字重时会自动重建
(见 docs/ENGINE_GUIDE.md §18.7)。
builtin::FontModal(字体弹窗)现在同时管字体族 + 字重:
FontModal { input, weight: &mut Weight, apply: &mut dyn FnMut(&str, Weight) },
字重下拉项来自 rjw_ui::FONT_WEIGHT_CHOICES(九档 100…900:超细/特细/细/常规/中等/半粗/粗/特粗/黑),
显示名 weight_label(w)。
⚠ weight 是草稿:下拉选中那一帧就写回(下拉是"点一次就收起"的控件,攒到"确定"会被
下一帧重置 ⇒ 症状"选不中任何其他字重");应用要把它与"已应用值"分开(打开时拷入、确定提交、
取消丢弃)。对话框里的预览用草稿字重排版(临时代换 Theme::font_weight 后还原;
RJ_FONT_TRACE=1 打印预览用的字重 + 样本实测宽)。
eg260818UI 里字重由弹窗写草稿、「确定」时经 apply 落到应用侧 TopBar::font_weight,
下一帧主题按它重建;--sim-weight(字重是排版输入)/ --sim-weight-modal(下拉真能换档 +
确定才提交)脚本化守护这两条路径。
投影令牌:PanelStyle::shadow: ShadowStyle { blur, offset, color }(色令牌 Palette::shadow),
主题级入口 Theme::with_shadow(ShadowStyle) / Theme::without_shadow(),逐容器入口
PanelStyle::{with_shadow, with_shadow_color, without_shadow}。blur = 0 = 不画(不是
Option)。颜色是任意色:顶点 RGB 原样带出、只有 alpha 按圈衰减 ⇒ alpha 管深浅
(深色预设 120 / 浅色 48)、RGB 管色相;blur > 0 而 alpha = 0 仍会镶嵌几何(看不见而已),
要省几何请归零 blur。见 §11「渲染增强」下方的说明。eg260818UI 的「主题调节」窗口里
「投影」滑杆后面那个色块就是它(可拖 alpha),--sim-shadow 脚本化守护这条通路。
缩放柄令牌:PanelStyle::grip: GripStyle { shape: GripShape, color, size, step, count }
—— 只对允许拖拽缩放的窗口(.resize(true),或没调 .resize 但设了 .width(..))生效,
且收起态一律不画也不响应(收起 = 一行标题栏,没有尺寸可调)。
GripShape::{Squares(默认,历史观感), Bars(**三条实心横杠**), Diagonal(**三条 45° 斜线**:首端点在同一水平线上等距、末端点在同一竖直线上等距), Hidden};
逐窗口入口 PanelStyle::{with_grip, with_grip_color, with_grip_shape, without_grip}。
Hidden 只是不画图案,拖动缩放照旧(命中区独立存在,跟随 size*step*count,下限 14px)。
Bars 用实心矩形(小尺寸下图标会被 Theme::feather 糊成一坨);Diagonal 只能走图标,
方框取 1.5× 免得三条斜线糊在一起。
边框归零(border_w = 0)的可见性:未勾选的 Checkbox 本来只画一圈描边,边框关掉后
会整个消失(标签看起来"没有控件")⇒ 此时改画实心底(surface_sunken / 悬停
surface_hover)。Segmented 的段间分隔线同样与 border_w 解耦(退化成 surface_dim)。
凡"只靠描边存在"的新控件都要提供第二视觉来源,见 docs/ENGINE_GUIDE.md §18.14。
子样式责任链:每个子样式都有 with_* builder setter(返回 Self,只改链上字段,
其余回落默认)——PanelStyle::default().with_radius(8.0) / ButtonStyle::default(). with_bg(c).with_radius(6.0) / SliderStyle::default().with_track(c).with_fill(c) 等,
与 Theme::with_* 同风格;font_family setter 接受 impl AsRef<str>(可直接传 &str /
&String / String),内部统一存 Arc<str>(跨样式 / 跨命令共享,克隆零字符串复制)。
样式可整体替换进主题(Theme::with_panel(..)),也用于容器 builder 的逐容器覆盖
(ui.window(..).style(panel_style))。
DPI 预乘:Ui::begin(..).scale_factor(s).build() 在 build() 时对最终 Theme 调用
Theme::scaled(全尺寸 / 字号字段 × s 取整,颜色 / 字体族
不变)——与 with_* 调用顺序无关。之后 Ui 内部以物理像素为单位;公开坐标 / 尺寸参数
用 Size / Position(默认 Logical,
× scale 换算;Size::Physical 原样)。widget builder 的数值覆盖(Label::font_size /
Button::radius/padding/font_size / Divider::thickness/margin 等)同样接收 Size。
圆角半径(
radius,逻辑像素,默认 0 = 直角):面板 / 窗口 / 按钮 / 输入框 / 勾选框(CheckboxStyle.radius)的背景与边框都由 CPU 把矩形镶嵌成三角形 (tess模块)——不生成任何纹理、不改着色器。
- 硬体轮廓
alpha = 1,同心的外圈轮廓alpha = 0,两者配成带状三角形, 由光栅化器插值出羽化边缘(这就是抗锯齿)。梯度以几何边缘为中心: 硬体内缩f/2、外环外扩f/2⇒ 视觉尺寸恒等于给定矩形。- 羽化宽 =
Theme::feather(逻辑像素,默认 1.0 ≈ 标准 1px 抗锯齿; 0 = 硬边;用with_feather(px)改,DPI 预乘为物理像素)。小控件自动收紧 (min(f, min(w,h)/4)),半径小到放不下两个同心轮廓时自动退回不羽化。- 「单位四分之一圆弧表 + 步长抽样」:一张 32 点细表服务所有半径—— 半径 → 目标弧距 2px →
stride(2 的幂)→segs = 32 / stride。N_FINE是 2 的幂 ⇒segs × stride恒等于N_FINE,四角弧首尾严丝合缝。- 带状化沿整圈推进(角的末点与下一角的首点在全局点号上相邻)⇒ 四条直边 自动被覆盖;按角分别成带会漏掉它们(曾经的表现:边框只剩 4 个圆角孤岛、 四条边整个消失,羽化也只在角上有效)。
- 背景颜色按顶点位置双线性 lerp:硬体每一个顶点(含圆角弧上的)都按自己在 矩形中的位置取色(
bilinear_color)。若弧上顶点直接带"本角颜色",渐变的两端会被 钉在弧的跨度上——200px 宽的胶囊 + 半径 18,左右各 18px 变成纯端色、整条斜坡被压进 中间 164px(肉眼:"两端发平、渐变被拉长")。羽化环则照抄硬体同序号顶点的颜色、 只把 alpha 置 0 ⇒ 纯 alpha 斜坡,不夹带色偏。- 半径不做取整(镶嵌器接受任意小数半径,超出半高时 clamp 成胶囊); 因此高 DPI 下不再有
radius × scale的取整误差问题。- 圆角边框是一圈环带(外轮廓与内轮廓之间,含四条直边;内半径按
max(0, r_outer - width),与 CSSborder-radius同规则)——边框的内外两条 边界都做羽化(最多四圈同心轮廓)。比"外圈实心 + 内圈实心"少一次边缘混合; 边框宽 ≥ 半边尺寸时退化成一块实心圆角矩形(语义一致)。- 边框颜色是调色板令牌(
Palette::border面板 / 分割线;Palette::border_strong按钮 / 输入框 / 勾选框),边框宽度用Theme::with_border_w(w)(逻辑像素, 级联panel/button/input/checkbox;0 = 不画)。Theme::with_radius(r)级联到全部有圆角的子样式:panel/button/input/checkbox(取r/2)/combo.menu_radius(取min(r, 6))。实时调参:
eg260818UI的「主题调节…」窗口可拖圆角 / 羽化 / 微渐变 / 背景 RGB / 边框 RGB / 边框宽 / 强调 RGB / 投影模糊宽,并一键切 dark / light / legacy 预设与紧凑 / 标准 / 宽松三档密度。投影(
ShadowStyle,v0.4 新增):从面板矩形向外blur像素铺SHADOW_STEPS = 4段 同心圆角带,第t圈外扩blur·t并偏移offset·t,alpha =a·(1−t)²(本体边缘 最浓、最外圈为 0,天然抗锯齿)。完全靠顶点色——无纹理、无着色器改动,进窗口顶点 缓存(内容不变时零开销),与背景同段合批。offset必须逐环分摊,只平移内轮廓会在 本体正下方留下一条等浓度暗带(看起来像"阴影下方突出");blur <= 0/ 颜色全透明 / 退化矩形 ⇒ 零几何。只挂PanelStyle(窗口 / 面板 / 浮层),薄控件不加投影。
| 字段 | 类型 / 默认 | 说明 |
|---|---|---|
layout_outline |
Color = 青色 |
debug_layout 布局描边颜色 |
layout_outline_width |
f32 = 1.0 |
debug_layout 描边宽度(物理像素) |
let mut theme = Theme::dark();
theme.debug.layout_outline = Color::MAGENTA; // 改描边颜色
theme.debug.layout_outline_width = 2.0; // 改描边宽度(物理像素)
// .theme(theme) 传入 UiDebugDraw 图元(
ui.debug_*)的样式 = 每次调用显式传参(color+width,逻辑像素); 需要统一样式时自建常量保存后传入。
| 字段 | 类型 / 默认 | 说明 |
|---|---|---|
color |
Color = 青色(dark 主题下亮青) |
当前焦点控件的描边颜色 |
width |
f32 = 1.0 |
焦点描边宽度(逻辑像素,内部 × scale 取整) |
交互控件(按钮 / 勾选 / 单选 / 滑块 / 输入框 / 下拉框 / 列表项按钮)每帧注册进
焦点链([rjw_ui::focus],按 (win, 录制序) 排序),UiState.focused 即当前焦点:
| 按键 | 行为 |
|---|---|
Tab |
焦点链下一个(环绕) |
Shift + Tab |
焦点链上一个(环绕) |
↑ / ↓ |
焦点链上 / 下一个(同 Shift+Tab / Tab) |
Enter / Space |
激活焦点控件:按钮点击、勾选/单选切换、下拉框展开/收起(输入框与滑块除外) |
← / → |
焦点为滑块时调值(步进 = 范围 5%);焦点为输入框时移动光标(原有) |
Esc |
收起展开的下拉框;否则取消焦点(输入框内原有行为不变;应用快捷键需在 text_focus() 为 None 时处理) |
- 焦点控件画一圈描边(
Theme::focus/FocusStyle),裁剪沿用控件自身(滚动容器内正确); - 焦点控件本帧未录制(窗口关闭等)自动清除焦点;
Tab从链首重新开始; - IME 组合中禁用方向键/Tab 焦点移动(上下键是输入法候选选择);点击其他窗口置顶时自动清除旧窗口的输入框焦点。
- 示例
eg260818UI:Tab 遍历主菜单 → 背包 → 窗口 A/B → 列表 → 下拉框,全程键盘可操作。
单行 text_input 与多行 text_area 共用能力(rjw_ui::edit 纯逻辑 + 单测):
| 能力 | 说明 |
|---|---|
| 超长滚动跟随光标 | 文本超出内容区时左移(WidgetState::text_scroll),光标右侧保留 8 逻辑像素;TextArea 垂直滚动(scroll_y)跟随光标行 + 滚轮 |
| 文本选择 | 按住拖选(WidgetState::sel_anchor;选择优先于窗口/面板拖拽——从输入框拖拽 = 选择文本);选择后打字 / 退格 / 删除 / 粘贴替换选择;拖出框外仍跟随 + edge-scroll(停在框外边缘自动滚动,"看不见的地方也选得到");纯单击(无位移)释放清理 anchor |
| 复制 / 粘贴 / 剪切 / 全选 | Ctrl+C / Ctrl+V / Ctrl+X / Ctrl+A(arboard 系统剪贴板;TextArea 跨行选择);单行粘贴过滤换行(多行拼接成一行) |
| Shift 选择 | Shift + ←/→/↑/↓/Home/End 扩展 / 收缩选择;无 Shift 方向键移动 = 单选(清除选择) |
| IME 组合候选浮动提示框 | 组合串(preedit)画在输入框下方浮动小框(底色 + 边框 + 灰色文本,自动宽度),不再占行内;系统候选框 set_ime_cursor_area 跟随光标(含水平/垂直滚动) |
| 多行 TextArea | p.text_area(id, &mut String) / text_area_at(id, rect, ...):Enter 换行、↑/↓ 跨视觉行(保持列)、Home/End 行首尾、按内容区宽度自动换行(create_buffer_wrap)、光标/点击/选择按视觉行定位与显示一致(Text::visual_lines)、行距 = Theme::line_spacing、跨视觉行选择高亮逐行绘制 |
- 输入框按下时置位
press_claimed:窗口/面板不建立拖拽基准(选择拖拽优先;窗口从空白/标题区拖动),并清除旧拖拽基准(防"瞬移"); - 主题:
InputStyle::sel_bg(选择高亮色,默认浅蓝 / dark 深蓝)。
| 函数 | 签名 | 说明 |
|---|---|---|
rounded_rect_at |
ui.rounded_rect_at(pos, size, radius, color) |
圆角矩形背景原语(radius 逻辑像素;CPU 镶嵌成三角形 + 1px 羽化,无纹理) |
gradient_rect_at |
ui.gradient_rect_at(pos, size, gradient) |
矩形渐变原语(绝对定位)。gradient 接受 Gradient 或 Color(Into) |
push_text_rect_ramp |
ui.push_text_rect_ramp(rect, text, size, color, family, align, valign, clip, buf, ramp) |
文本绘制原语(带首末两色渐变):TextRamp({from, to, axis, mode},Copy)在某个域里取色,再与 color(tint)逐分量相乘 ⇒ 逐顶点渐变、零纹理、零着色器改动。域 mode:Glyph(逐字形)/ Line(逐行,默认)/ Frame(整块,即"Text 域")。采样口径三条:①域与采样点都在文本视觉原点系(不换算到窗口/绝对坐标);②采样点用未裁剪字形几何取"字形内归一化位置" ⇒ 裁剪不改变颜色;③ramp = None 与 push_text_rect 逐位等价。Painter::text_ramp / draw::text_cmd_ramp 是同层入口(text_cmd 旧签名不变,内部填 None)。⚠ ramp(含域)进内容签名(gpu_batch::cmd_sig_hash)⇒ 改两色 / 方向 / 域都会重建窗口顶点缓存 |
gradient_rect |
ui.gradient_rect(size, gradient) |
同上,但位置来自当前容器游标(随布局流) |
icon_at |
ui.icon_at(pos, size, icon, color) |
矢量图标(绝对定位;size 为方框,非方形时按 min(w,h) 居中等比) |
icon |
ui.icon(size, icon, color) |
同上,但位置来自当前容器游标——row 内连续调用即得工具栏 |
Icon |
Check / ChevronUp / ChevronDown / ChevronLeft / ChevronRight / Close / Grip / Warning |
画出来的几何(单位方框 [0,1]² 内的凸分片,Icon::parts() 公开;Close = 两条顺时针凸平行四边形拼的 ×,标题栏关闭按钮用) |
image_at |
ui.image_at(pos, size, bg) |
背景图(绝对定位;bg: ImageBg 决定铺排 / 染色 / 圆角遮罩) |
image |
ui.image(size, bg) |
同上,但位置来自当前容器游标 |
ImageBg |
new(tex, texel) + .fit(..) / .radius(..) / .tint(..) |
纹理 uid + 纹素尺寸 + 铺排 + 染色 + 圆角遮罩;PanelStyle::with_bg_image 可直接当窗口/面板底图 |
ImageFit |
Stretch / Fill / Center / Tile |
拉伸 / 等比覆盖(居中裁剪)/ 原始尺寸居中 / 1:1 平铺 |
Gradient |
pure(c) / vertical(top, bottom) / horizontal(left, right) / rotated(from, to, angle) / corners(tl, tr, bl, br) |
四角颜色(pub tl/tr/bl/br);From<Color> 给纯色 |
lerp_color |
lerp_color(a, b, k) |
颜色线性插值(Gradient 构造器与四角采样的基础) |
矢量图标不需要字体:▾ / ✓ / ≡ 这类字符的可用性、宽度、基线全由字体决定
(缺字形会走 fallback,甚至会因字体不同而宽高不一)。Icon 改为在单位方框内给出
凸多边形分片,由 crate::tess::push_convex 扇形三角化 + 按 Theme::feather
做边缘羽化(与圆角矩形同一套顶点 alpha 插值机制,DrawKind::Icon 走同一条批)。
自定义图标请按 Icon::parts() 的形状(单位方框、凸、屏幕顺时针)提供分片。
// 使用前
use rjw_krusie::prelude::*;
// 1) 纯色(等价实心)
ui.gradient_rect_at(pos, size, Color::from_hex("#3af")); // Color: Into<Gradient>
// 2) 上下 / 左右双色
ui.gradient_rect_at(pos, size, Gradient::vertical(Color::RED, Color::BLUE));
ui.gradient_rect_at(pos, size, Gradient::horizontal(Color::RED, Color::BLUE));
// 3) 任意角度(0° = 下→上,90° = 左→右,逆时针为正)
ui.gradient_rect_at(pos, size, Gradient::rotated(Color::RED, Color::BLUE, 0.5));
// 4) 四角各异(双线性;1D 纹理表达不了)
ui.gradient_rect_at(pos, size, Gradient::corners(a, b, c, d));
// 5) 矢量图标
ui.icon_at(pos, Vec2::splat(16.0), Icon::Check, Color::WHITE);
ui.row(|r| r.icon(Vec2::splat(18.0), Icon::ChevronDown, Color::WHITE)); // 工具栏
// 6) 背景图(`tex` = TextureWrapped::uid,`texel` = 纹素尺寸)
let bg = ImageBg::new(tex, Vec2::new(64.0, 64.0)); // 默认 Stretch
ui.image_at(pos, size, bg.fit(ImageFit::Fill).radius(8.0)); // 等比覆盖 + 圆角遮罩
ui.image_at(pos, size, bg.fit(ImageFit::Tile)); // 1:1 平铺(直角)
ui.window("w").style(ui.theme().panel.clone().with_bg_image(bg)); // 当窗口底图背景图与圆角遮罩为什么能共存:Stretch / Fill / Center 的 UV 是顶点位置的
仿射映射,扇形三角化下的重心插值精确再现它 ⇒ 圆角硬体 + 羽化带上的每个顶点都带
正确的 UV,边缘由羽化带的 alpha 斜坡裁掉(真正的圆角遮罩,不是把直角图片贴上去)。
整个特性零着色器改动、零额外 draw call(图片按纹理切段,与字形 / 白纹理各一段;
几何进窗口顶点缓存,跨帧复用)。Tile 用逐块四边形实现(1:1,边缘部分块按比例截断
UV),不支持圆角遮罩——平铺需要 UV 环绕(u > 1),那要求批次携带 Repeat
采样器(UiBatch 目前不带 RStates);块数超 MAX_IMAGE_TILES 时退化为拉伸。
内联只占一行(色块 + #RRGGBB + 展开箭头),点开是独立置顶窗口面板:
| 项 | 签名 | 说明 |
|---|---|---|
ColorPicker |
ColorPicker::new(id, &mut Color) |
主构造(颜色直接写在 &mut Color 上,"变没变"由调用方前后比较) |
.alpha(on) |
面板里多一行 Alpha(默认只 RGB) | |
.with_hex(&mut String) |
可选:把顶部文本框绑到调用方缓冲;不传则用全局跨帧缓冲 | |
.size(s) |
impl Into<Size<Vec2>> |
入口色块的尺寸(Logical/Physical;默认 input.min_w × 22)。只影响内联那一行——面板宽另有主题口径,两者解耦 |
.popup_width(w) |
面板宽(物理像素;默认 = 主题 input.min_w × 1.9 与"通道行最小宽"取大) |
|
ColorFormat |
U8 / Hex / F |
文本框呈现格式(全局偏好,所有取色器一致) |
ColorPickerState |
UiState::color_picker |
全局跨帧数据:mode / text(替补缓冲)/ open(同时只有一个面板)/ HSV 缓存 |
format_color |
format_color(c, mode, with_alpha) |
按模式呈现(255, 0, 0 / #FF00AA / 1.00, 0.00, 0.00) |
parse_color |
parse_color(text, mode) |
自动识别格式:先按当前模式,再试另两种;None = 不可识别(不改颜色) |
面板内容:模式行(u8/HEX/F)+ 文本框(不可识别时右侧出现 Icon::Warning 按钮,按下恢复
有效值)+ HSV 区(SV 平面 + 6 段色相条)+ 通道行(颜色滑块 + NumberInput)+ 可选 A 行。
入口尺寸与面板尺寸解耦:.size(..) 只改内联色块;面板宽默认取 Theme::input.min_w × 1.9
(主题口径)而与入口实测宽无关——否则把入口调大会顺手把对话框也撑宽。--sim-picker
第 88/92 帧钉住这条(入口改成 200×40 后,面板宽必须仍是 401)。
通道精度:u8 / HEX 是整数通道(步进 1);F 模式用 step(0.001)(3 位小数)——比
NumberInput 的默认精度(0.01 = 2 位)更细,因为 8 位通道相邻字节只差 1/255 ≈ 0.004。
SV 平面 = 一个四角顶点色的圆角矩形([白, 纯色相, 黑, 黑] 的双线性插值恰好等于
HSV 公式)⇒ 无纹理、无着色器改动、无额外 draw call。实现按职责拆在
widgets/colorpicker/{format,hsv,state,panel}.rs(纯函数各自带单测)。
use rjw_krusie::prelude::*;
ui.add(ColorPicker::new("tint", &mut color).alpha(true));
// 只想要文本解析 / 呈现(不画控件):
let c = parse_color("#FF00AA", ColorFormat::U8).unwrap();
assert_eq!(format_color(c, ColorFormat::F, false), "1.00, 0.00, 0.67");渐变不需要纹理(v0.3 起):顶点格式 VertexP3U2C4 自带 4 分量顶点色,
光栅化器本就做重心插值 ⇒ 一个 quad + 白纹理即可,管线零改动。这一决定替代了旧实现
(把渐变烘成 1×64 条纹纹理塞进动态图集)。旧实现的代价:每帧一次 String 建 key
({t:.3} 还会静默撞键)、permanent 条目让图集永久无法 repack_all、
每次纹理切换多一次 draw call、且单轴纹理表达不了四角各异的颜色。
- 不支持多段 stops(3+ 停靠点):四角顶点色是双线性的,无法精确表达多段。
多段渐变请用
rjw_text::Gradient(作用于文字,本就支持多段; 见Gradient::glyph_h/glyph_v/line_h/line_v/frame_h/frame_v)。 - 裁剪保锚:矩形被裁剪时四角色按其在原矩形中的相对位置重采样,
颜色的空间锚定不变(否则裁剪会让渐变整体平移)。⚠ 重采样前必须把命令的绝对
矩形换算到与裁剪结果相同的窗口局部空间(
resample_gradient_local)—— 混用会让 u/v 整体偏心窗口原点,渐变被平移甚至外推出界。 - 圆角 + 渐变天然共存:圆角镶嵌直接吃四角色(
RoundedRectSpec.corners), 所以Brush的两端色与圆角是同一套顶点色路径,不需要专门着色器,也不需要纹理。 GradientAxis现在只属于文字渐变(rjw_text::GradientAxis), 不再是矩形渐变的参数;rjw_ui根不再导出它(rjw_ui::text::GradientAxis仍可用)。- 提交分组为
(win, 图形/文字组, 纹理 uid):渐变与圆角都属于图形组(白纹理), 先于文字; ⚠ UI 的 Render2D 必须set_sort_mode(SortMode::None)(完全按提交顺序绘制)——SortMode::LayerAndStates会按纹理 uid 重排而盖住文字(示例eg260818UI即如此配置); - 控件级集成:
Theme的PanelStyle::radius/ButtonStyle::radius/InputStyle::radius/CheckboxStyle.radius,背景色则统一是 [Brush](纯色 / 两端色渐变,见下节)。
PanelStyle::bg / ButtonStyle::{bg,bg_hover,bg_pressed} / InputStyle::bg 的类型是
Brush(Color: Into<Brush> ⇒ 既有 with_bg(Color::RED) 调用点不用改):
pub enum Brush { Solid(Color), Vertical(Color, Color), Horizontal(Color, Color) }Brush::corners() -> [Color; 4]是所有绘制路径的统一输入,与圆角天然共存。- 只有两端色:多段 stops 的能力在
Gradient(显式原语,支持rotated/ 四角各异) 与rjw_text::Gradient(文字)上;主题默认值要便宜、好维护。 Brush::as_solid()让"两端同色"退化回纯色路径;PartialEq<Color>让theme.panel.bg == Color::RED这类断言可直接写。- 表面微渐变由
Palette.bevel+bevel_raised/bevel_sunken从一个表面色派生: 深色取 0.10(面板/按钮上亮下暗、输入框上暗下亮),浅色取 0.02,legacy_dark取 0。
Theme::themed(&Palette) 从一份调色板组装整套主题;Theme::{light,dark,dark_legacy}
是它的三个预设。字段按层次命名(surface_dim < surface_sunken 例外 < surface <
surface_raised < surface_overlay < surface_hover < surface_active),
一套明暗阶梯服务全部控件。换肤只需换一份 Palette:
let mut p = rjw_ui::Palette::dark();
p.accent = Color::rgba_u8(255, 120, 200, 255);
let theme = rjw_ui::Theme::themed(&p);Theme::palette() 返回组装来源(手工改过字段后不代表实际颜色)。
Palette::legacy_dark() + Theme::dark_legacy() 精确复刻 v0.3 的硬编码深色配色。
| 函数 | 签名 | 说明 |
|---|---|---|
debug_layout |
ui.debug_layout(true) / .debug_layout(true)(构建期) |
每个控件/容器矩形画描边(布局 + 命中区域可视化;样式见 DebugStyle;开启时跳过窗口顶点缓存) |
debug_line |
ui.debug_line(a, b, width, color) |
屏幕空间线段(绝对逻辑像素,覆盖在 UI 内容之上) |
debug_rect_outline |
ui.debug_rect_outline(&Rect, width, color) |
屏幕空间矩形框 |
debug_circle_outline |
ui.debug_circle_outline(center, r, segments, width, color) |
屏幕空间圆环 |
debug_cross |
ui.debug_cross(center, half, width, color) |
屏幕空间十字标记 |
debug_grid |
ui.debug_grid(&Rect, spacing, width, color) |
屏幕空间网格(每方向 ≤ 512 条) |
window_order |
ui.window_order() -> Vec<(String, u32)> |
诊断:窗口 z 序(按 z 升序) |
window_under_mouse |
ui.window_under_mouse() -> Option<(String, u32)> |
诊断:鼠标下最上层窗口(重叠点击时唯一可交互的窗口) |
UiState::last_press_window |
state.last_press_window() -> Option<(&str, u32)> |
诊断:上次按下由哪个窗口接收(重叠点击"赢家") |
UiState::occluded_hits |
state.occluded_hits() -> u32 |
诊断:上帧命中但被更高窗口遮挡而未响应的控件次数(点击穿透拦截计数) |
UiState::widget_occluded_hits |
state.widget_occluded_hits() -> u32 |
诊断:上帧命中但被同窗口内更上层控件遮挡而未响应的次数(控件级遮挡拦截计数) |
UiState::press_cancelled_by_window |
state.press_cancelled_by_window() -> u32 |
诊断:上帧认领按下后被帧末复核撤销的次数——命中那一刻被判"没被遮挡"、而帧末完备的遮挡表表明它其实被更高 z 的窗口盖住了(见 Ui::resolve_widget_press;应为 0) |
UiStats::prologue_us |
stats.prologue_us(f64,µs) |
各段开场耗时(懒开场 / 冻结输入 / 装载帧级事实 / 建根容器)。与 finish_us 一起把 ui_frame_us 三分解:prologue + 应用录制 + finish |
世界坐标调试图元(游戏场景:碰撞盒 / 网格 / 速度矢量)见
rjw_2d_render::debug_draw(draw_line/draw_rect_outline/draw_circle_outline/draw_circle_filled/draw_cross/draw_grid)。示例:examples/egDebugDraw(rjw_ui 屏幕空间 + 世界空间 + debug_layout)、examples/eg260818UI(右上角窗口诊断面板)。窗口遮挡(点击穿透)已修复:重叠区域只有鼠标下最上层窗口的控件响应——
window_occluded判定(hit.rs),窗口矩形跨帧缓存于UiState.window_rects;occluded_hits > 0即证明 背后控件被正确抑制。
运行时路径(推荐):一帧可开任意多段(f.ui(theme)),位置随意。
fn update(&mut self, ctx: &mut Ctx) {
let Some(mut f) = ctx.frame() else { return };
f.draw().sprite(...); // 世界层
let mut ui = f.ui(Theme::dark()); // 段 1
ui.pack_at(Vec2::new(16.0, 16.0), PackSide::Top, |p| {
if p.button("start", "开始游戏").clicked() { /* ... */ }
self.volume = p.slider("vol", 0.0..=1.0, self.volume);
if p.checkbox("fs", "全屏", self.fs).toggled() { self.fs = !self.fs; }
p.text_input("name", &mut self.name);
});
ui.finish(); // 段收尾(可省略:作用域结束即收尾)
f.text(|t| { /* 世界文本(段之间随便交错) */ });
let mut hud = f.ui(Theme::dark()); // 段 2(同一帧)
hud.label_at(Vec2::new(16.0, 690.0), "HUD");
hud.finish();
f.submit(&mut self.cam, Clear::color(Color::rgb(0.05, 0.05, 0.08)));
}低层路径(自己持有 UiState,自定义 base_layer / 复用别的 Text):
use rjw_ui::{IdAbsolute, PackSide, Theme, Ui, UiState};
let mut state = UiState::new();
state
.radio_groups
.insert("diff".into(), IdAbsolute::from("diff_normal")); // 默认选中
// 每帧(window/font 来自主循环;输入设备经 capture 快照;相机/渲染器延迟到收尾):
state.begin_frame(); // 帧级账由调用方负责(每帧一次)
let mut ui = Ui::begin(window, &mut font, &mut state)
.capture(&ctx.mouse, &ctx.keyboard)
.theme(Theme::dark()).build();
ui.pack_at(Vec2::new(16.0, 16.0), PackSide::Top, |p| {
if p.button("start", "开始游戏").clicked() { /* ... */ }
volume = p.slider("vol", 0.0..=1.0, volume);
if p.checkbox("fs", "全屏", fs).toggled() { fs = !fs; }
p.text_input("name", &mut name);
});
ui.end_frame(r2d); // 帧收尾(焦点导航/描边 + 光标 + 统计 + 提交)约定:交互控件 ID 必须稳定;顶层 pack 控件(
label/button/…)经根容器(build()内建,可用宽 = 视口宽)直接流式堆叠,绝对定位用*_at;控件坐标 = 屏幕逻辑像素 (.scale_factor设置 DPI,不设置则等于物理像素);文本输入支持中文 IME (ctx.keys().ime_commits()/ime_preedit(),候选框跟随光标);输入框聚焦时用ui.state().text_focus()屏蔽应用快捷键; 控件文本排版缓冲自持于UiState.text_buffers(CachePolicy::User,不推入rjw_textLRU); 独立 UI 渲染:UI 录到 UI 层自己的 Render2D(set_sort_mode(SortMode::None)关闭排序),与世界encode合并提交(一次 present);Ui::finish按(win, depth, 图形/文字, 录制序)免排序(win + depth 分桶)逐段提交,Ui::end_frame做帧收尾。
还想看更多?源码在 crates/rjw_*/src/,目录与本文一一对应。