GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: beveled plate borders with a specular tint (focused-pane highlight)
Under control_relief a bordered plate renders as an SDF-lit bevel
(style.surface.plate.bevel_width roll, default 6) instead of the flat
border line — the plates' counterpart of the controls' outline->relief
degradation; blur-behind negative alpha resolves in the plate branch as
before. Prim::Bevel carries a specular tint (PaintCtx::bevel_tinted,
append_widget_plate_tinted): PlatePush grows a specular_tint vec4 —
push constants 112 -> 128 bytes, exactly the Vulkan-guaranteed minimum
— and shader2d multiplies the roll and sphere specular terms by it.
Neutral white everywhere by default; a host marks a plate (focus) by
coloring its glint instead of ringing it.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 10 +++++++---
src/color.rs | 20 ++++++++++++++++++++
src/scene/paint.rs | 15 ++++++++++++---
src/scene/painter.rs | 19 +++++++++++++++++--
src/vk/renderer.rs | 15 ++++++++++-----
src/vk/shader2d.wgsl | 7 +++++--
src/widget/model.rs | 2 +-
7 files changed, 72 insertions(+), 16 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 56b5648..601c142 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1513,7 +1513,7 @@ pub fn tessellate_display_list(
push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, *fill, no, None, &mut verts);
push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
}
- Prim::Bevel { rect, radii, color, depth } if shader_plates => {
+ Prim::Bevel { rect, radii, color, depth, tint } if shader_plates => {
// SDF-lit raised plate: one cover quad; the shader owns fill,
// roll shading, corners, and silhouette AA. Nominal corner
// radii (scale_corners false): a Bevel is a WIDGET-scale plate
@@ -1521,7 +1521,9 @@ pub fn tessellate_display_list(
// the controls around it — only window-scale `Plate`s get the
// curvature-matched span.
verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
- plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false));
+ let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false);
+ p.specular_tint = [tint[0], tint[1], tint[2], 0.0];
+ plate = Some(p);
made_plate = Some(*rect);
}
Prim::Plate { rect, radii, color, depth } if shader_plates => {
@@ -1641,7 +1643,7 @@ pub fn tessellate_display_list(
];
plate = Some(p);
}
- Prim::Bevel { rect, radii, color, depth } => {
+ Prim::Bevel { rect, radii, color, depth, tint: _ } => {
// Full-size fill: the lip is now a shading overlay, not a paint of the
// outer ring, so the fill must cover the whole rect (the old inset fill
// would leave the ring showing whatever lay beneath).
@@ -1737,6 +1739,7 @@ pub fn tessellate_display_list(
light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
material: plate_mat,
host: [0.0; 4],
+ specular_tint: [1.0, 1.0, 1.0, 0.0],
mode: 5.0,
shape: 2.0,
});
@@ -1829,6 +1832,7 @@ fn plate_push_raised(
// Mode-1 semantics: [feature offset, feature count] — no carves yet;
// the tessellator fills these in as recesses group into this plate.
host: [0.0, 0.0, 0.0, 0.0],
+ specular_tint: [1.0, 1.0, 1.0, 0.0],
mode: 1.0,
shape,
}
diff --git a/src/color.rs b/src/color.rs
index 0b4a32d..3bb23b6 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -633,6 +633,9 @@ fn parse_and_set_colors(content: &str) {
if let Some(t) = val.pointer("/style/surface/plate/border_thickness").and_then(|v| v.as_f64()) {
if let Ok(mut lock) = PLATE_BORDER_THICKNESS.write() { *lock = t as f32; }
}
+ if let Some(t) = val.pointer("/style/surface/plate/bevel_width").and_then(|v| v.as_f64()) {
+ if let Ok(mut lock) = PLATE_BEVEL_WIDTH.write() { *lock = t as f32; }
+ }
if let Some(blur) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_bool()) {
if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur; }
} else if let Some(blur_val) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_f64()) {
@@ -1675,6 +1678,9 @@ pub fn set_backplate_statusbar_blur(b: bool) {
static PLATE_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.15, 0.15, 0.2, 0.95]));
static PLATE_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.3, 0.3, 0.4, 1.0]));
static PLATE_BORDER_THICKNESS: RwLock<f32> = RwLock::new(1.0);
+/// Roll width of the beveled plate border (the control_relief replacement for
+/// the flat border line) — `style.surface.plate.bevel_width`.
+static PLATE_BEVEL_WIDTH: RwLock<f32> = RwLock::new(6.0);
pub fn plate_color() -> Option<[f32; 4]> {
load_colors_once();
@@ -1718,6 +1724,20 @@ pub fn set_plate_border_thickness(t: f32) {
}
}
+pub fn plate_bevel_width() -> f32 {
+ load_colors_once();
+ if let Ok(lock) = PLATE_BEVEL_WIDTH.read() {
+ return *lock;
+ }
+ 6.0
+}
+
+pub fn set_plate_bevel_width(t: f32) {
+ if let Ok(mut lock) = PLATE_BEVEL_WIDTH.write() {
+ *lock = t;
+ }
+}
+
static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
pub fn plate_blur() -> bool {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 57a83ef..585fc80 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -41,8 +41,12 @@ pub enum Prim {
/// `push_widget_vertices`' non-bevel branch: rounded bg + `push_plate_solid_border_vertices`).
Border { rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32 },
/// A beveled plate: a rounded fill at full size plus a light/shadow overlay lip
- /// (mirrors `push_widget_vertices`' bevel branch).
- Bevel { rect: Rect, radii: Radii, color: [f32; 4], depth: f32 },
+ /// (mirrors `push_widget_vertices`' bevel branch). `tint` multiplies the lit
+ /// roll's specular color — neutral white normally; a host sets it to a
+ /// highlight color to mark the plate (the focused-pane treatment) without a
+ /// separate border ring. Shader-plates path only; the legacy banded
+ /// tessellation ignores it.
+ Bevel { rect: Rect, radii: Radii, color: [f32; 4], depth: f32, tint: [f32; 3] },
/// A recess carved into whatever is already painted underneath — the inverse of
/// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
/// through the middle: a relief cut into the backplate rather than a plate laid on
@@ -370,8 +374,13 @@ impl PaintCtx {
}
pub fn bevel(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
+ self.bevel_tinted(rect, radii, color, depth, [1.0, 1.0, 1.0]);
+ }
+
+ /// `bevel` with a specular tint — see `Prim::Bevel::tint`.
+ pub fn bevel_tinted(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32, tint: [f32; 3]) {
let rect = self.apply_offset(rect);
- self.push(Prim::Bevel { rect, radii, color, depth });
+ self.push(Prim::Bevel { rect, radii, color, depth, tint });
}
/// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index 06a75bf..edf36d3 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -88,14 +88,29 @@ pub fn paint_legacy_leaf(
/// hand-build their display list in their own draw order (the designer) instead of walking
/// `paint_self`, but want a widget's background exactly as the vertex path drew it.
pub fn append_widget_plate(w: &dyn WidgetHost, pc: &mut PaintCtx) {
+ append_widget_plate_tinted(w, pc, None);
+}
+
+/// [`append_widget_plate`] with an optional specular tint for the plate's
+/// bevel — the focused-pane treatment: the highlight colors the lit roll's
+/// glint instead of drawing a separate border ring. Under `control_relief` a
+/// bordered plate renders as a bevel (`plate_bevel_width` roll) — the beveled
+/// counterpart of the flat border line, exactly the controls' own
+/// outline→relief degradation.
+pub fn append_widget_plate_tinted(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Option<[f32; 3]>) {
let (x, y, ww, h) = w.rect();
let rect = Rect { x, y, width: ww, height: h };
let radii = w.corner_radii();
let radii_tuple = (radii.top_left, radii.top_right, radii.bottom_right, radii.bottom_left);
+ let tint = tint.unwrap_or([1.0, 1.0, 1.0]);
if let Some(thickness) = w.plate_bevel() {
- pc.bevel(rect, radii_tuple, w.color(), thickness);
+ pc.bevel_tinted(rect, radii_tuple, w.color(), thickness, tint);
} else if let Some((border_color, thickness)) = w.solid_border() {
- pc.border(rect, radii_tuple, w.color(), border_color, thickness);
+ if crate::layout::control_relief() {
+ pc.bevel_tinted(rect, radii_tuple, w.color(), crate::colors::plate_bevel_width(), tint);
+ } else {
+ pc.border(rect, radii_tuple, w.color(), border_color, thickness);
+ }
} else {
pc.border(rect, radii_tuple, w.color(), [0.0; 4], 0.0);
}
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 0d5420a..f3680be 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -54,6 +54,9 @@ pub struct PlatePush {
/// (center + half-extents) a free recess fades out against; far-away sides
/// (±1e5) disable the fade.
pub host: [f32; 4],
+ /// RGB multiplies the roll's specular color (w unused). Neutral white
+ /// normally; the focused-pane bevel carries the highlight color here.
+ pub specular_tint: [f32; 4],
/// 1.0 = raised lit plate, 2.0 = recess overlay.
pub mode: f32,
/// Corner shape exponent: 2.0 = circular arcs, > 2 = superellipse
@@ -498,12 +501,13 @@ impl VkRenderer {
let set_layouts = [descriptor_set_layout];
// Push constants: the per-batch rounded-rect clip plus the SDF-lit
- // plate block (seven vec4s, matching shader2d's `RRectClip`), read by
- // shader2d's fragment stage.
+ // plate block (eight vec4s, matching shader2d's `RRectClip`), read by
+ // shader2d's fragment stage. 128 bytes — exactly the Vulkan-guaranteed
+ // minimum budget.
let push_ranges = [vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.offset(0)
- .size(112)];
+ .size(128)];
let pipeline_layout = device
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default()
@@ -1484,7 +1488,7 @@ impl VkRenderer {
// batch is a plate cover quad.
let rr = batch.clip_rrect.unwrap_or([0.0; 5]);
let enabled = if batch.clip_rrect.is_some() { 1.0f32 } else { 0.0 };
- let mut pc = [0.0f32; 28];
+ let mut pc = [0.0f32; 32];
pc[..5].copy_from_slice(&rr);
pc[5] = enabled;
pc[7] = clip_shape;
@@ -1496,6 +1500,7 @@ impl VkRenderer {
pc[16..20].copy_from_slice(&p.light);
pc[20..24].copy_from_slice(&p.material);
pc[24..28].copy_from_slice(&p.host);
+ pc[28..32].copy_from_slice(&p.specular_tint);
if p.mode == 1.0 {
// Rebase the feature offset onto this
// frame's UBO slot.
@@ -1551,7 +1556,7 @@ impl VkRenderer {
.cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
// Push constants persist across binds — clear any batch's
// rounded clip and plate mode.
- let pc = [0.0f32; 28];
+ let pc = [0.0f32; 32];
self.core.device.cmd_push_constants(
cmd,
self.pipeline_layout,
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index ed9855b..d7aad3d 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -94,6 +94,9 @@ struct RRectClip {
// the carve fades out against — a wall flush with the host's edge dies
// across the host's perimeter roll; far-away sides sit at ±1e5 (no fade).
p_host: vec4f,
+ // RGB multiplies the lit roll's specular color — neutral white normally,
+ // a highlight color on a marked (focused) plate. w unused.
+ p_spec_tint: vec4f,
}
var<push_constant> rrect_clip: RRectClip;
@@ -239,7 +242,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
let shade = 1.0 + (diff / flat_shade - 1.0) * strength;
let spec = roll_spec(c / h);
- return vec4f(vcol.rgb * shade + vec3f(spec * strength), vcol.a * aa);
+ return vec4f(vcol.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), vcol.a * aa);
}
let gd = rr_sdf_grad(frag, rrect_clip.p_rect, rrect_clip.p_radii);
@@ -283,7 +286,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
let shade = 1.0 + (diff / flat_shade - 1.0 + extra) * strength;
let spec = roll_spec(sv);
- return vec4f(base.rgb * shade + vec3f(spec * strength), abs(base.a) * aa);
+ return vec4f(base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), abs(base.a) * aa);
}
// Free-floating recess, boss, or ridge (one not grouped into a host plate —
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 5304bd7..07a6ef4 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1234,7 +1234,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
Prim::Quad { rect, color } => ctx.quad(rect, color),
Prim::RoundedRect { rect, radius, corners, color } => ctx.rounded_rect(rect, radius, corners, color),
Prim::Border { rect, radii, fill, border, thickness } => ctx.border(rect, radii, fill, border, thickness),
- Prim::Bevel { rect, radii, color, depth } => ctx.bevel(rect, radii, color, depth),
+ Prim::Bevel { rect, radii, color, depth, tint } => ctx.bevel_tinted(rect, radii, color, depth, tint),
Prim::Recess { rect, radii, depth, edges } => ctx.recess_edges(rect, radii, depth, edges),
Prim::Boss { rect, radii, depth, edges } => ctx.boss_edges(rect, radii, depth, edges),
Prim::Ridge { rect, radii, depth, edges } => ctx.ridge_edges(rect, radii, depth, edges),