GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
The plate prims carry a Material (RFC material, step 2b)
Prim::Plate, Bevel, Sphere, Droplet and DropletScrim carry a Material
instead of an encoded colour, and PaintCtx::plate / plate_shaped / bevel
/ bevel_tinted / sphere / droplet / droplet_scrim take one. The
tessellator reads each prim's vertex colour as material.fill(Nested)
and its push-constant finish as material.finish — the DE finish
(plate_mat) is now only the carves', which shade whatever is beneath
them. plate_spec emits spec.material.for_role(role): a root plate's
frost is the compositor's, so its prim is opaque on this side.
The Droplet stops being the hand-packed exception: DropletSpec::finish()
puts gleam / shine / rim in the specular / shininess / curvature slots
of a Finish, the status bar emits its drops with that finish on the
material, and the runner pushes material.finish like any plate's.
Material::from_fill is the bridge for a call site still holding an
encoded colour (a negative alpha decodes to Frosted at the DE recipe):
from_fill(c).fill(Nested) == c for every c the old API took.
Material::for_role resolves the root regime and agrees with fill().
Prim-identical: tests/plate_golden.rs against the dump taken before
step 2a — identical, 148518 lines.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/backend/window_runner.rs | 100 +++++++++++++++++++++-------------
src/bin/cce-ramp.rs | 2 +-
src/bin/cce-relief.rs | 4 +-
src/layout.rs | 3 +-
src/scene/material.rs | 39 +++++++++++++
src/scene/paint.rs | 82 ++++++++++++++++------------
src/scene/painter.rs | 4 +-
src/widget/container/parameters_bg.rs | 2 +-
src/widget/container/scroll_box.rs | 2 +-
src/widget/core.rs | 2 +-
src/widget/display/info_box.rs | 2 +-
src/widget/mod.rs | 2 +-
tests/plate_golden.rs | 20 +++----
13 files changed, 171 insertions(+), 93 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b1ee2e9..a48b17c 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1794,6 +1794,7 @@ pub fn tessellate_display_list(
sh: f32,
scale: f32,
) -> (Vec<Vertex>, Vec<DlBatch>, Vec<DlImage>, Vec<[f32; 12]>) {
+ use crate::scene::material::PlateRole;
use crate::scene::paint::{Cap, Prim};
let mut verts: Vec<Vertex> = Vec::new();
let mut batches: Vec<DlBatch> = Vec::new();
@@ -1833,10 +1834,13 @@ pub fn tessellate_display_list(
// draw a different material than the renderer applies.
let plate_light = crate::scene::relief_shade::light_vector();
// [shading strength (1.0 at the default bevel_depth), specular strength,
- // shininess, curvature/AO strength] — the plastic material. Curvature is
- // kept near the raised path's crest amplitude: the recess shoulder's
- // brightening lands on the same pixels as its specular line, and the two
- // stack — at 0.5 the step read several times hotter than a plate roll.
+ // shininess, curvature/AO strength] — the DE's finish, for the CARVES,
+ // which shade whatever is beneath them and so take the host's. A prim
+ // that carries a Material (Plate, Bevel, Sphere, Droplet) pushes its own
+ // `material.finish` instead. Curvature is kept near the raised path's
+ // crest amplitude: the recess shoulder's brightening lands on the same
+ // pixels as its specular line, and the two stack — at 0.5 the step read
+ // several times hotter than a plate roll.
let plate_mat = crate::scene::material::Finish::from_style().to_array();
for item in &dl.items {
@@ -1853,11 +1857,14 @@ pub fn tessellate_display_list(
// context menu (Plate) worked.
let blur_behind = matches!(
&item.prim,
- crate::scene::paint::Prim::Bevel { color, .. }
- | crate::scene::paint::Prim::Plate { color, .. }
- | crate::scene::paint::Prim::Droplet { color, .. }
- | crate::scene::paint::Prim::Quad { color, .. }
+ crate::scene::paint::Prim::Quad { color, .. }
| crate::scene::paint::Prim::RoundedRect { color, .. } if color[3] < 0.0
+ ) || matches!(
+ &item.prim,
+ crate::scene::paint::Prim::Bevel { material, .. }
+ | crate::scene::paint::Prim::Plate { material, .. }
+ | crate::scene::paint::Prim::Droplet { material, .. }
+ if material.fill(PlateRole::Nested)[3] < 0.0
) || matches!(
&item.prim,
crate::scene::paint::Prim::Border { fill, .. } if fill[3] < 0.0
@@ -1905,15 +1912,17 @@ pub fn tessellate_display_list(
Prim::Glow { rect, radius, reach, color } => {
push_glow_vertices(rect.x, rect.y, rect.width, rect.height, *radius, *reach, sw, sh, *color, no, &mut verts);
}
- Prim::Bevel { rect, radii, color, depth, tint } if shader_plates => {
+ Prim::Bevel { rect, radii, material, depth, tint } if shader_plates => {
+ let color = material.fill(PlateRole::Nested);
+ let mat = material.finish.to_array();
// 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
// whose silhouette must match the nominal-radius squircles of
// 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));
- let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false, None);
+ verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
+ let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, false, None);
// w = 1 marks an accent-tinted plate (the focused-pane
// treatment): the shader then colors the WHOLE rolled edge
// with the tint, not just the specular glint — matching the
@@ -1924,7 +1933,9 @@ pub fn tessellate_display_list(
plate = Some(p);
made_plate = Some(*rect);
}
- Prim::Plate { rect, radii, color, depth, shape } if shader_plates => {
+ Prim::Plate { rect, radii, material, depth, shape } if shader_plates => {
+ let color = material.fill(PlateRole::Nested);
+ let mat = material.finish.to_array();
if *depth < 0.0 {
// Negative depth = fill-less roll overlay (MODE_ROLL): the
// window-edge roll shading alone, screened over whatever is
@@ -1933,14 +1944,14 @@ pub fn tessellate_display_list(
// and the batch is NOT opened as a carve host: an overlay
// owns no surface for a CSG feature to cut into.
verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
- let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, plate_mat, true, *shape);
+ let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, mat, true, *shape);
p.mode = 11.0; // MODE_ROLL
plate = Some(p);
} else {
// Same lit-plate branch; the cover quad is the exact rect so the
// silhouette and the compositor's rounded window corners agree.
- 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, true, *shape));
+ 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, mat, true, *shape));
made_plate = Some(*rect);
}
}
@@ -2197,7 +2208,8 @@ pub fn tessellate_display_list(
];
plate = Some(p);
}
- Prim::Bevel { rect, radii, color, depth, tint: _ } => {
+ Prim::Bevel { rect, radii, material, depth, tint: _ } => {
+ let color = material.fill(PlateRole::Nested);
// 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).
@@ -2205,10 +2217,11 @@ pub fn tessellate_display_list(
top_left: radii.0, top_right: radii.1,
bottom_right: radii.2, bottom_left: radii.3,
};
- push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, *color, no, None, &mut verts);
- push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, *color, no, &mut verts);
+ push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts);
+ push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, color, no, &mut verts);
}
- Prim::Plate { rect, radii, color, depth, .. } => {
+ Prim::Plate { rect, radii, material, depth, .. } => {
+ let color = material.fill(PlateRole::Nested);
if *depth < 0.0 {
// Fill-less roll overlay (negative-depth sentinel): the banded
// legacy tessellation has no overlay compositing, so the roll
@@ -2225,12 +2238,12 @@ pub fn tessellate_display_list(
bottom_right: radii.2, bottom_left: radii.3,
};
push_rounded_rect_vertices_corners(
- rect.x, rect.y, rect.width, rect.height, corners, sw, sh, *color, no, None, &mut verts,
+ rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts,
);
push_plate_face_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, no, &mut verts);
push_bevel_edge_vertices_radii(
rect.x, rect.y, rect.width, rect.height, *radii, *depth,
- sw, sh, *color, no, 1.0, &mut verts,
+ sw, sh, color, no, 1.0, &mut verts,
);
}
Prim::Recess { rect, radii, depth, edges, .. } => {
@@ -2322,36 +2335,41 @@ pub fn tessellate_display_list(
verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, *color, segs(*radius), no));
}
}
- Prim::Sphere { cx, cy, radius, color } if shader_plates => {
+ Prim::Sphere { cx, cy, radius, material } if shader_plates => {
+ let color = material.fill(PlateRole::Nested);
+ let mat = material.finish.to_array();
// A hemisphere lit per pixel by the plate branch (mode 5): one
// cover quad, its own never-merged batch. The quad overhangs
// the disc by 1px for the shader's silhouette anti-aliasing.
let d = *radius + 1.0;
- verts.extend(quad_vertices(cx - d, cy - d, 2.0 * d, 2.0 * d, sw, sh, *color));
+ verts.extend(quad_vertices(cx - d, cy - d, 2.0 * d, 2.0 * d, sw, sh, color));
plate = Some(crate::vk::PlatePush {
// Center + radius in physical px; the SDF box machinery is
// unused in this mode, so .w is free.
rect: [cx * scale, cy * scale, radius * scale, 0.0],
radii: [0.0; 4],
light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
- material: plate_mat,
+ material: mat,
host: [0.0; 4],
specular_tint: [1.0, 1.0, 1.0, 0.0],
mode: 5.0,
shape: 2.0,
});
}
- Prim::Sphere { cx, cy, radius, color } => {
+ Prim::Sphere { cx, cy, radius, material } => {
+ let color = material.fill(PlateRole::Nested);
// Legacy path: the flat disc, exactly a Circle.
- verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, *color, segs(*radius), no));
+ verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, color, segs(*radius), no));
}
- Prim::DropletScrim { rect, color, spec, feather } if shader_plates => {
+ Prim::DropletScrim { rect, material, spec, feather } if shader_plates => {
+ let color = material.fill(PlateRole::Nested);
+ let mat = material.finish.to_array();
// Shader mode 12: the droplet's own SDF, filled flat and
// feathered inward. No contact shadow, so unlike the lit drop
// the cover quad is exactly the box — a scrim never draws
// outside the silhouette.
let g = droplet_geom(rect, spec);
- verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
+ verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
plate = Some(crate::vk::PlatePush {
rect: [
(rect.x + rect.width * 0.5) * scale,
@@ -2363,14 +2381,16 @@ pub fn tessellate_display_list(
// p_light.w carries the FEATHER here; mode 12 returns
// before the shading band it otherwise holds is read.
light: [plate_light[0], plate_light[1], plate_light[2], feather.max(0.001) * scale],
- material: [plate_mat[0], 0.0, 0.0, 0.0],
+ material: [mat[0], 0.0, 0.0, 0.0],
host: [g.sr * scale, 0.0, 0.0, g.ar * scale],
specular_tint: [0.0, 0.0, 0.0, g.bow * scale],
mode: 12.0,
shape: spec.curve.clamp(2.0, 6.0),
});
}
- Prim::Droplet { rect, color, spec } if shader_plates => {
+ Prim::Droplet { rect, material, spec } if shader_plates => {
+ let color = material.fill(PlateRole::Nested);
+ let mat = material.finish.to_array();
// A water droplet lit by shader mode 10: one cover quad; the
// shader owns silhouette (sheet ∪smin belly), dome shading,
// fresnel rim and thin-edge clarity. The spec's height
@@ -2388,7 +2408,7 @@ pub fn tessellate_display_list(
rect.y,
rect.width + 2.0 * sh_reach,
rect.height + sh_reach,
- sw, sh, *color,
+ sw, sh, color,
));
plate = Some(crate::vk::PlatePush {
rect: [
@@ -2400,9 +2420,11 @@ pub fn tessellate_display_list(
radii: [sag * scale, br * scale, bw * scale, k * scale],
light: [plate_light[0], plate_light[1], plate_light[2], band * scale],
// Slots y/z/w feed roll_spec and the rim term directly:
- // droplets carry their own gleam/shine/rim instead of the
- // DE material's (a drop is wetter than the DE's plates).
- material: [plate_mat[0], spec.gleam, spec.shine, spec.rim],
+ // a droplet's material carries its own gleam/shine/rim
+ // there (`DropletSpec::finish`; a drop is wetter than the
+ // DE's plates), so this is the material's finish like any
+ // plate's.
+ material: mat,
host: [sr * scale, spec.clarity.clamp(0.0, 1.0), spec.dome, ar * scale],
// Droplet glints are always white, so the tint RGB slots
// carry droplet params instead: x = core density,
@@ -2417,7 +2439,8 @@ pub fn tessellate_display_list(
shape: spec.curve.clamp(2.0, 6.0),
});
}
- Prim::DropletScrim { rect, color, spec, .. } => {
+ Prim::DropletScrim { rect, material, spec, .. } => {
+ let color = material.fill(PlateRole::Nested);
// Legacy banded path: no SDF to feather against, so the scrim
// degrades to the same flat outline the drop itself does —
// hard-edged, but present. A prim with no arm here VANISHES.
@@ -2425,9 +2448,10 @@ pub fn tessellate_display_list(
let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
- push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, *color, no, None, &mut verts);
+ push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, color, no, None, &mut verts);
}
- Prim::Droplet { rect, color, spec } => {
+ Prim::Droplet { rect, material, spec } => {
+ let color = material.fill(PlateRole::Nested);
// Legacy banded path: the flat drop outline — attach-tapered
// top, round bottom. Degrades the material but keeps the
// silhouette (a prim with no arm here VANISHES, it doesn't
@@ -2436,7 +2460,7 @@ pub fn tessellate_display_list(
let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
- push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, *color, no, None, &mut verts);
+ push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, color, no, None, &mut verts);
}
Prim::ConcaveFillet { cx, cy, radius, depth, start: a0, raised } if shader_plates => {
// A quarter-arc carve wall (shader mode 6/7): one cover quad
diff --git a/src/bin/cce-ramp.rs b/src/bin/cce-ramp.rs
index 26bf998..397769a 100644
--- a/src/bin/cce-ramp.rs
+++ b/src/bin/cce-ramp.rs
@@ -258,7 +258,7 @@ impl Application for RampPopup {
pc.plate(
Rect { x: m, y: m, width: w - 2.0 * m, height: h - 2.0 * m },
(radius, radius, radius, radius),
- plate,
+ &cce_ui::scene::Material::from_fill(plate),
bevel,
);
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index cbc22bf..d224e1a 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -911,7 +911,7 @@ fn draw_section(pc: &mut PaintCtx, rect: Rect, profile: &ProfileKnobs, shape: Sh
Shape::EdgeRoll => pc.plate(
Rect { x: x1 - 4000.0, y: swatch_y - 400.0, width: 4000.0, height: strip_h + 800.0 },
sq,
- [plate[0], plate[1], plate[2], 1.0],
+ &cce_ui::scene::Material::from_fill([plate[0], plate[1], plate[2], 1.0]),
unit,
),
}
@@ -1563,7 +1563,7 @@ impl Application for BevelPopup {
pc.plate(
Rect { x: 0.0, y: 0.0, width: w, height: h },
(radius, radius, radius, radius),
- plate,
+ &cce_ui::scene::Material::from_fill(plate),
bevel,
);
diff --git a/src/layout.rs b/src/layout.rs
index 8488572..8e9ccf4 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4223,7 +4223,8 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
None => pc.inset_plate(face, rect.x, rect.y, rect.width, rect.height, r, depth),
}
}
- Prim::Bevel { rect, radii, color, .. } => {
+ Prim::Bevel { rect, radii, material, .. } => {
+ let color = material.fill(crate::scene::material::PlateRole::Nested);
if color[3].abs() > 0.001 {
emit_rounded(pc, rect, radii, color);
}
diff --git a/src/scene/material.rs b/src/scene/material.rs
index f860e8e..8b493af 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -182,6 +182,29 @@ impl Material {
Self::opaque(crate::color::button_background_color())
}
+ /// The legacy bridge: a fill as the renderer consumed it before this
+ /// type existed. A negative alpha is the frost sentinel — `Frosted` at
+ /// the DE recipe, tint alpha `|a|`; otherwise `Opaque` with the colour
+ /// as is. `from_fill(c).fill(Nested) == c` for every `c` a caller could
+ /// hand the old API. For a call site that holds an encoded colour; a
+ /// site that knows what it means says `Material::opaque` / `with_frost`.
+ pub fn from_fill(encoded: [f32; 4]) -> Self {
+ let a = encoded[3];
+ let m = Self::opaque([encoded[0], encoded[1], encoded[2], a.abs()]);
+ if a < 0.0 { m.with_frost(Frost::from_style()) } else { m }
+ }
+
+ /// This material as a plate in `role` carries it: a root plate's frost
+ /// is the COMPOSITOR's, so under [`PlateRole::Root`] the client-side
+ /// material is opaque — the prim a `PlateSpec` emits carries this, and
+ /// the tessellator encodes every prim as nested.
+ pub fn for_role(&self, role: PlateRole) -> Self {
+ match role {
+ PlateRole::Root => Material { frost: Frost::Opaque, ..*self },
+ PlateRole::Nested => *self,
+ }
+ }
+
// ---- derived materials -------------------------------------------------
/// The well floor cut into this plate: the same material with the tint
@@ -329,6 +352,22 @@ mod tests {
assert_eq!(m.flat_control(), m);
}
+ /// The legacy bridge round-trips every fill the old API accepted, and
+ /// the role resolution agrees with the encoding function.
+ #[test]
+ fn from_fill_round_trips_and_for_role_matches_fill() {
+ for c in [[0.1, 0.2, 0.3, 0.8], [0.1, 0.2, 0.3, -0.8], [0.0; 4], [0.5, 0.5, 0.5, 1.0]] {
+ let m = Material::from_fill(c);
+ assert_eq!(m.fill(PlateRole::Nested), c, "{c:?}");
+ assert_eq!(m.frost.is_frosted(), c[3] < 0.0);
+ assert!(m.tint[3] >= 0.0, "tint alpha is never negative");
+ for role in [PlateRole::Root, PlateRole::Nested] {
+ assert_eq!(m.for_role(role).fill(PlateRole::Nested), m.fill(role), "{c:?} {role:?}");
+ }
+ }
+ assert_eq!(Material::from_fill([0.0, 0.0, 0.0, -0.5]).frost, Frost::from_style());
+ }
+
/// The control-face rule: opaque or nothing.
#[test]
fn control_face_is_opaque_or_none() {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 6a28d3a..e6b988f 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -18,7 +18,7 @@
//! follow-ups.
use crate::scene::layout::Rect;
-use crate::scene::material::{Material, PlateRole};
+use crate::scene::material::{Finish, Material, PlateRole};
/// End-cap style for a [`Prim::Vector`], mirroring the toolkit's line caps.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -414,6 +414,18 @@ impl DropletSpec {
}
}
+impl DropletSpec {
+ /// The drop's finish: its own gleam, shine and rim in the specular,
+ /// shininess and curvature slots of a [`Finish`] (a drop is wetter than
+ /// the DE's plates), the shading strength the DE's. The material a
+ /// droplet is emitted with carries this — `Material::from_fill(c)
+ /// .with_finish(spec.finish())` — and the tessellator reads it from
+ /// there like any plate's, instead of packing the slots by hand.
+ pub fn finish(&self) -> Finish {
+ Finish { spec: self.gleam, shininess: self.shine, curvature: self.rim, ..Finish::from_style() }
+ }
+}
+
impl Default for DropletSpec {
fn default() -> Self {
// The oval dewdrop: no belly, no sag — one continuous curve from a
@@ -458,7 +470,7 @@ pub enum Prim {
/// 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] },
+ Bevel { rect: Rect, radii: Radii, material: Material, 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 root plate rather than a plate laid on
@@ -528,7 +540,7 @@ pub enum Prim {
/// for this one plate — `Some(2.0)` is circular arcs, so a plate whose
/// radii reach its half-extent is a true circle regardless of the
/// squircle the rest of the DE wears. `None` follows the DE.
- Plate { rect: Rect, radii: Radii, color: [f32; 4], depth: f32, shape: Option<f32> },
+ Plate { rect: Rect, radii: Radii, material: Material, depth: f32, shape: Option<f32> },
Arc { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4] },
/// A ring band with radial color interpolation — inner rim → crest
/// (centerline) → outer rim — for rounded rim bevels (the Ramp's key
@@ -550,7 +562,7 @@ pub enum Prim {
/// color is the sphere's face color exactly at the lit center, like a
/// plate's face keeps the app's color. Falls back to a flat circle on the
/// legacy (`bevel_shader 0`) path.
- Sphere { cx: f32, cy: f32, radius: f32, color: [f32; 4] },
+ Sphere { cx: f32, cy: f32, radius: f32, material: Material },
/// A hanging water droplet clinging to the TOP edge of `rect`, lit per pixel
/// by shader mode 10: the silhouette is a smooth union of a film "sheet"
/// attached to the top edge (square top corners — the attach line) and a
@@ -563,7 +575,7 @@ pub enum Prim {
/// plastic). Shape knobs in [`DropletSpec`]. On the legacy (`bevel_shader
/// 0`) path it degrades to the flat hanging capsule — square top, round
/// bottom — rather than vanishing.
- Droplet { rect: Rect, color: [f32; 4], spec: DropletSpec },
+ Droplet { rect: Rect, material: Material, spec: DropletSpec },
/// The same silhouette as [`Prim::Droplet`] under the same [`DropletSpec`],
/// filled FLAT and feathered inward: opaque through the interior, fading
/// to nothing over `feather` px as it approaches the drop's edge. A
@@ -575,7 +587,7 @@ pub enum Prim {
/// outline with a rounded rect, so the two can never disagree about where
/// the drop's edge is. On the legacy (`bevel_shader 0`) path it degrades
/// to the same flat rounded-rect outline `Prim::Droplet` falls back to.
- DropletScrim { rect: Rect, color: [f32; 4], spec: DropletSpec, feather: f32 },
+ DropletScrim { rect: Rect, material: Material, spec: DropletSpec, feather: f32 },
/// A concave inside-corner fillet for composed carves: a quarter-arc wall
/// whose centre `(cx, cy)` sits out in the corner's pocket, shaded with the
/// same step profile as a `Recess`/`Boss` wall (`raised` flips the sign).
@@ -941,23 +953,24 @@ impl PaintCtx {
}
/// A sphere-lit circle — see `Prim::Sphere`.
- pub fn sphere(&mut self, cx: f32, cy: f32, radius: f32, color: [f32; 4]) {
+ pub fn sphere(&mut self, cx: f32, cy: f32, radius: f32, material: &Material) {
let (ox, oy) = self.offset;
- self.push(Prim::Sphere { cx: cx + ox, cy: cy + oy, radius, color });
+ self.push(Prim::Sphere { cx: cx + ox, cy: cy + oy, radius, material: *material });
}
/// A hanging water droplet clinging to `rect`'s top edge — see
/// [`Prim::Droplet`] and [`DropletSpec`].
/// See [`Prim::DropletScrim`]. `feather` is how far in from the drop's
/// edge the fill reaches full opacity, in logical px.
- pub fn droplet_scrim(&mut self, rect: Rect, color: [f32; 4], spec: DropletSpec, feather: f32) {
+ pub fn droplet_scrim(&mut self, rect: Rect, material: &Material, spec: DropletSpec, feather: f32) {
let rect = self.apply_offset(rect);
- self.push(Prim::DropletScrim { rect, color, spec, feather });
+ self.push(Prim::DropletScrim { rect, material: *material, spec, feather });
}
- pub fn droplet(&mut self, rect: Rect, color: [f32; 4], spec: DropletSpec) {
+ /// The drop's finish is the material's (see [`DropletSpec::finish`]).
+ pub fn droplet(&mut self, rect: Rect, material: &Material, spec: DropletSpec) {
let rect = self.apply_offset(rect);
- self.push(Prim::Droplet { rect, color, spec });
+ self.push(Prim::Droplet { rect, material: *material, spec });
}
/// A concave inside-corner fillet — see `Prim::ConcaveFillet`. `start` is
@@ -991,8 +1004,8 @@ impl PaintCtx {
Prim::Border { rect, radii, fill, border, thickness } => {
self.border(rect, radii, fill, border, thickness)
}
- Prim::Bevel { rect, radii, color, depth, tint } => {
- self.bevel_tinted(rect, radii, color, depth, tint)
+ Prim::Bevel { rect, radii, material, depth, tint } => {
+ self.bevel_tinted(rect, radii, &material, depth, tint)
}
Prim::Recess { rect, radii, depth, edges, tint } => match tint {
Some(t) => self.recess_tinted(rect, radii, depth, t),
@@ -1007,8 +1020,8 @@ impl PaintCtx {
Some(t) => self.trough_tinted(rect, radii, depth, t),
None => self.trough_edges(rect, radii, depth, edges),
},
- Prim::Plate { rect, radii, color, depth, shape } => {
- self.plate_shaped(rect, radii, color, depth, shape)
+ Prim::Plate { rect, radii, material, depth, shape } => {
+ self.plate_shaped(rect, radii, &material, depth, shape)
}
Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
self.arc(cx, cy, radius, thickness, start, end, color)
@@ -1020,11 +1033,11 @@ impl PaintCtx {
self.vector(x1, y1, x2, y2, thickness, color, cap)
}
Prim::Circle { cx, cy, radius, color } => self.circle(cx, cy, radius, color),
- Prim::Sphere { cx, cy, radius, color } => self.sphere(cx, cy, radius, color),
+ Prim::Sphere { cx, cy, radius, material } => self.sphere(cx, cy, radius, &material),
Prim::Glow { rect, radius, reach, color } => self.glow(rect, radius, reach, color),
- Prim::Droplet { rect, color, spec } => self.droplet(rect, color, spec),
- Prim::DropletScrim { rect, color, spec, feather } => {
- self.droplet_scrim(rect, color, spec, feather)
+ Prim::Droplet { rect, material, spec } => self.droplet(rect, &material, spec),
+ Prim::DropletScrim { rect, material, spec, feather } => {
+ self.droplet_scrim(rect, &material, spec, feather)
}
Prim::ConcaveFillet { cx, cy, radius, depth, start, raised } => {
self.concave_fillet(cx, cy, radius, depth, start, raised)
@@ -1085,14 +1098,14 @@ impl PaintCtx {
self.push(Prim::Border { rect, radii, fill, border, thickness });
}
- 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]);
+ pub fn bevel(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32) {
+ self.bevel_tinted(rect, radii, material, 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]) {
+ pub fn bevel_tinted(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, tint: [f32; 3]) {
let rect = self.apply_offset(rect);
- self.push(Prim::Bevel { rect, radii, color, depth, tint });
+ self.push(Prim::Bevel { rect, radii, material: *material, depth, tint });
}
/// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
@@ -1152,9 +1165,10 @@ impl PaintCtx {
PlateStance::Raised => {
// abs(): a negative alpha is the frost sentinel, a real face.
if plate.face[3].abs() > 0.001 {
+ let face = Material::from_fill(plate.face);
match plate.tint {
- Some(t) => self.bevel_tinted(plate.rect, plate.radii, plate.face, plate.depth, t),
- None => self.bevel(plate.rect, plate.radii, plate.face, plate.depth),
+ Some(t) => self.bevel_tinted(plate.rect, plate.radii, &face, plate.depth, t),
+ None => self.bevel(plate.rect, plate.radii, &face, plate.depth),
}
} else {
let (plateau, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
@@ -1438,18 +1452,18 @@ impl PaintCtx {
/// rolled perimeter (width `-depth`) renders as an overlay — translucent
/// white screen / black multiply — over whatever is beneath, for a root
/// plate whose face is not a fill (the designer's full-bleed 3D canvas).
- /// `color` is ignored; the roll profile, crest and specular are exactly the
+ /// `material` is ignored; the roll profile, crest and specular are exactly the
/// positive-depth plate's.
- pub fn plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
- self.plate_shaped(rect, radii, color, depth, None);
+ pub fn plate(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32) {
+ self.plate_shaped(rect, radii, material, depth, None);
}
/// [`plate`](Self::plate) with an explicit corner exponent — see
/// [`Prim::Plate`]'s `shape`. `Some(2.0)` on a plate whose radii are its
/// half-extent draws a circle; `None` is exactly `plate`.
- pub fn plate_shaped(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32, shape: Option<f32>) {
+ pub fn plate_shaped(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, shape: Option<f32>) {
let rect = self.apply_offset(rect);
- self.push(Prim::Plate { rect, radii, color, depth, shape });
+ self.push(Prim::Plate { rect, radii, material: *material, depth, shape });
}
/// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner
@@ -1472,7 +1486,7 @@ impl PaintCtx {
pub fn plate_spec(&mut self, spec: &PlateSpec) {
let f = crate::layout::corner_span_factor();
let (tl, tr, br, bl) = spec.radii();
- self.plate(spec.rect, (tl / f, tr / f, br / f, bl / f), spec.fill(), spec.depth);
+ self.plate(spec.rect, (tl / f, tr / f, br / f, bl / f), &spec.material.for_role(spec.role()), spec.depth);
}
pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
@@ -1863,7 +1877,7 @@ mod tests {
ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
ctx.rounded_rect(r(0.0, 0.0, 1.0, 1.0), 2.0, (true, false, true, false), [0.0; 4]);
ctx.border(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), [0.1; 4], [0.9; 4], 1.5);
- ctx.bevel(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), [0.3; 4], 2.0);
+ ctx.bevel(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), &Material::opaque([0.3; 4]), 2.0);
ctx.arc(5.0, 5.0, 4.0, 1.0, 0.0, 3.14, [0.0; 4]);
ctx.vector(0.0, 0.0, 10.0, 0.0, 1.0, [0.0; 4], Cap::Arrow);
ctx.circle(5.0, 5.0, 3.0, [0.0; 4]);
@@ -1876,7 +1890,7 @@ mod tests {
let mut ctx = PaintCtx::new();
ctx.translate(10.0, 20.0, |ctx| {
ctx.border(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), [0.0; 4], [1.0; 4], 1.0);
- ctx.bevel(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), [0.0; 4], 1.0);
+ ctx.bevel(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), &Material::opaque([0.0; 4]), 1.0);
});
let list = ctx.finish();
assert!(matches!(list.items[0].prim, Prim::Border { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index 21314f8..a3f216a 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -112,10 +112,10 @@ pub fn append_widget_plate_radii(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Op
let rect = Rect { x, y, width: ww, height: h };
let tint = tint.unwrap_or([1.0, 1.0, 1.0]);
if let Some(thickness) = w.plate_bevel() {
- pc.bevel_tinted(rect, radii_tuple, w.color(), thickness, tint);
+ pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), thickness, tint);
} else if let Some((border_color, thickness)) = w.solid_border() {
if crate::layout::control_relief() {
- pc.bevel_tinted(rect, radii_tuple, w.color(), crate::colors::plate_bevel_width(), tint);
+ pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), crate::colors::plate_bevel_width(), tint);
} else {
pc.border(rect, radii_tuple, w.color(), border_color, thickness);
}
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index d92cdcf..ac9909a 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1506,7 +1506,7 @@ impl Paint for ParametersBg {
ctx.concave_fillet(fcx, fcy, fr, fd, fs, false);
}
for (scx, scy, sr, sc) in self.spheres() {
- ctx.sphere(scx, scy, sr, sc);
+ ctx.sphere(scx, scy, sr, &crate::scene::material::Material::from_fill(sc));
}
self.paint_scene_rows(ctx);
for (qx, qy, qw, qh, qc) in self.plain_quads() {
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index f9175ab..124df75 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -49,7 +49,7 @@ pub fn paint_relief_scrollbar(
pc.bevel(
Rect { x: sb_x, y: thumb_y, width: sb_w, height: thumb_h },
radii,
- crate::color::scrollbar_thumb_color(),
+ &crate::scene::material::Material::from_fill(crate::color::scrollbar_thumb_color()),
depth,
);
}
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 8128dcf..297d855 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -522,7 +522,7 @@ pub mod context_menu {
// menu_opacity, not the color's own alpha: an opaque page
// color resolved the frost to a solid tint (invisible).
frosted[3] = -crate::color::menu_opacity();
- ctx.plate(rect, (r, r, r, r), frosted, depth);
+ ctx.plate(rect, (r, r, r, r), &crate::scene::material::Material::from_fill(frosted), depth);
} else {
let (plateau, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
ctx.boss(plateau, radii, depth);
diff --git a/src/widget/display/info_box.rs b/src/widget/display/info_box.rs
index 601e223..6b2ccad 100644
--- a/src/widget/display/info_box.rs
+++ b/src/widget/display/info_box.rs
@@ -42,7 +42,7 @@ impl Paint for InfoBox {
// control wall, capped by its height like every plate's.
let fill = colors::plate_color().unwrap_or_else(|| colors::active_theme().surface_bg);
let depth = crate::layout::bevel_width().min(rect.height * 0.2);
- ctx.plate(rect, radii, fill, depth);
+ ctx.plate(rect, radii, &crate::scene::material::Material::from_fill(fill), depth);
} else {
// Flat: the themed surface in a hairline frame.
let theme = colors::active_theme();
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 03b9166..88499e2 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -409,7 +409,7 @@ pub trait WidgetHost {
let cr = self.corner_radii();
let radii = (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left);
if let Some(depth) = self.plate_bevel() {
- ctx.bevel(rect, radii, color, depth);
+ ctx.bevel(rect, radii, &crate::scene::material::Material::from_fill(color), depth);
} else if let Some((border_color, thickness)) = self.solid_border() {
ctx.border(rect, radii, color, border_color, thickness);
} else if color[3].abs() > 0.001 {
diff --git a/tests/plate_golden.rs b/tests/plate_golden.rs
index 51aea06..13c259e 100644
--- a/tests/plate_golden.rs
+++ b/tests/plate_golden.rs
@@ -82,17 +82,17 @@ fn scene(sw: f32, sh: f32) -> cce_ui::scene::paint::DisplayList {
window_corners: (true, true, true, true),
depth: -depth,
});
- pc.plate_shaped(r(30.0, 170.0, 40.0, 40.0), (20.0, 20.0, 20.0, 20.0), [0.3, 0.3, 0.35, 1.0], 4.0, Some(2.0));
+ pc.plate_shaped(r(30.0, 170.0, 40.0, 40.0), (20.0, 20.0, 20.0, 20.0), &cce_ui::scene::Material::from_fill([0.3, 0.3, 0.35, 1.0]), 4.0, Some(2.0));
// Bare plates the legacy callers emit: an opaque face, a frosted one
// (negative alpha handed straight in, the menu idiom).
- pc.plate(r(80.0, 170.0, 60.0, 30.0), rr, [0.13, 0.14, 0.16, 1.0], 4.0);
- pc.plate(r(150.0, 170.0, 60.0, 30.0), rr, [0.13, 0.14, 0.16, -0.8], 4.0);
+ pc.plate(r(80.0, 170.0, 60.0, 30.0), rr, &cce_ui::scene::Material::from_fill([0.13, 0.14, 0.16, 1.0]), 4.0);
+ pc.plate(r(150.0, 170.0, 60.0, 30.0), rr, &cce_ui::scene::Material::from_fill([0.13, 0.14, 0.16, -0.8]), 4.0);
// Bevels, plain and tinted (the focused-pane ring).
- pc.bevel(r(220.0, 170.0, 60.0, 30.0), rr, [0.25, 0.25, 0.3, 1.0], 4.0);
- pc.bevel_tinted(r(290.0, 170.0, 60.0, 30.0), rr, [0.25, 0.25, 0.3, 1.0], 4.0, [0.4, 0.6, 1.0]);
- pc.bevel_tinted(r(290.0, 205.0, 60.0, 30.0), rr, [0.0; 4], 4.0, [0.4, 0.6, 1.0]);
+ pc.bevel(r(220.0, 170.0, 60.0, 30.0), rr, &cce_ui::scene::Material::from_fill([0.25, 0.25, 0.3, 1.0]), 4.0);
+ pc.bevel_tinted(r(290.0, 170.0, 60.0, 30.0), rr, &cce_ui::scene::Material::from_fill([0.25, 0.25, 0.3, 1.0]), 4.0, [0.4, 0.6, 1.0]);
+ pc.bevel_tinted(r(290.0, 205.0, 60.0, 30.0), rr, &cce_ui::scene::Material::from_fill([0.0; 4]), 4.0, [0.4, 0.6, 1.0]);
// Every control stance × face × tint.
let faces: [[f32; 4]; 3] = [[0.3, 0.3, 0.36, 1.0], [0.0; 4], [0.3, 0.3, 0.36, -0.6]];
@@ -120,11 +120,11 @@ fn scene(sw: f32, sh: f32) -> cce_ui::scene::paint::DisplayList {
// The lit ball and the water: default spec, and one with its own finish
// and depth terms.
- pc.sphere(40.0, 340.0, 10.0, [0.4, 0.4, 0.5, 1.0]);
- pc.droplet(r(80.0, 330.0, 120.0, 30.0), [0.1, 0.1, 0.12, -0.7], DropletSpec::default());
+ pc.sphere(40.0, 340.0, 10.0, &cce_ui::scene::Material::from_fill([0.4, 0.4, 0.5, 1.0]));
+ pc.droplet(r(80.0, 330.0, 120.0, 30.0), &cce_ui::scene::Material::from_fill([0.1, 0.1, 0.12, -0.7]).with_finish(DropletSpec::default().finish()), DropletSpec::default());
let wet = DropletSpec::parse("gleam=1.0 shine=16 rim=0.3 clarity=0.5 core=0.4 shadow=0.3 refr=2");
- pc.droplet(r(220.0, 330.0, 120.0, 30.0), [0.1, 0.1, 0.12, 0.9], wet);
- pc.droplet_scrim(r(220.0, 365.0, 120.0, 30.0), [0.1, 0.1, 0.12, 0.9], wet, 3.0);
+ pc.droplet(r(220.0, 330.0, 120.0, 30.0), &cce_ui::scene::Material::from_fill([0.1, 0.1, 0.12, 0.9]).with_finish(wet.finish()), wet);
+ pc.droplet_scrim(r(220.0, 365.0, 120.0, 30.0), &cce_ui::scene::Material::from_fill([0.1, 0.1, 0.12, 0.9]), wet, 3.0);
pc.finish()
}