GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: cce-bevel shows lit cross-sections shaped by sliders, not ramps
Each profile renders as a cutaway of the actual edge — material slab in
the plate color inside a dark opening, surface stroked with per-segment
lighting from the DE azimuth; the carve section runs plateau→wall→floor,
the roll ends at the silhouette's cut face. Shape comes from three
semantic knobs (Shoulder / Base / Bias) through the rational
two-exponent ease h(w)=w^a/(w^a+(1-w)^b) over a bias pre-warp —
midpoints reproduce the analytic smoothstep, extremes give sharp
chamfers or wide round-overs. Curves sample into 17-key linear ramp
specs, so the config format and DE-wide loader are untouched and
free-form specs from cce-designer still load; knob triples persist as
style.surface.relief.profile_knobs / edge_knobs so the editor reopens
where it left off. Untouched sections save the identity sentinel
(analytic). The overflow rim is gone — no key pegs to spill.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/bin/cce-bevel.rs | 468 ++++++++++++++++++++++++++++++++++-----------------
src/layout.rs | 4 +
2 files changed, 322 insertions(+), 150 deletions(-)
diff --git a/src/bin/cce-bevel.rs b/src/bin/cce-bevel.rs
index 5dd1945..4e255d4 100644
--- a/src/bin/cce-bevel.rs
+++ b/src/bin/cce-bevel.rs
@@ -1,29 +1,30 @@
-//! `cce-bevel` — the relief-material control interface: two [`Ramp`] editors
-//! shaping the DE's bevel profiles (the wall curve every recess/boss carve
-//! renders, and the plate perimeter's roll curve), with the companion
-//! depth/width knobs. Every edit applies live to this process — the popup's
-//! own plate, wells, and buttons ARE the preview — and logs to stdout;
-//! Save persists to `~/.config/cce/config.kdl` (`style.surface.relief`) so
-//! every cce app starts with the styled walls.
+//! `cce-bevel` — the relief-material control interface. Each profile (the
+//! wall curve every recess/boss carve renders, and the plate perimeter's
+//! roll) is shown as a lit CROSS-SECTION of the actual edge — plateau, wall,
+//! floor — and shaped by three semantic sliders (Shoulder / Base / Bias)
+//! instead of a free-form ramp. Every edit applies live to this process (the
+//! popup's own plate, wells, and buttons ARE the preview) and logs the
+//! sampled spec to stdout; Save persists to `~/.config/cce/config.kdl`
+//! (`style.surface.relief`) so every cce app starts with the material.
//!
-//! Architecture mirrors `cce-ramp` (single-purpose popup) with the DemoApp
-//! multi-root event routing.
+//! The curve family is the two-exponent rational ease
+//! `h(w) = w^a / (w^a + (1-w)^b)` over a bias pre-warp `w = v^g` — monotone,
+//! endpoint-exact, with the shoulder (a) and base fillet (b) shaped
+//! independently. Slider midpoints give a=b=2, g=1: the analytic smoothstep.
+//! Curves are sampled into ramp-spec keys, so the config format and the
+//! DE-wide loader are unchanged — free-form specs from cce-designer or a
+//! hand-edited config still load everywhere.
use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
use cce_ui::layout::RELIEF_PROFILE_IDENTITY_SPEC as IDENTITY_SPEC;
use cce_ui::scene::layout::Rect;
-use cce_ui::scene::paint::{DisplayList, PaintCtx};
+use cce_ui::scene::paint::{Cap, DisplayList, PaintCtx};
use cce_ui::widget::{
- Adapted, Button, ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, Ramp, Slider,
+ Adapted, Button, ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, Slider,
WidgetHost, WidgetId,
};
use wayland_client::QueueHandle;
-/// Transparent rim between the surface edge and the plate: room for the
-/// ramps' key pegs to render outside the window frame instead of being
-/// clipped at the buffer edge.
-const OVERFLOW_MARGIN: f32 = 40.0;
-
const HEADER_FONT_SIZE: f32 = 13.0;
const HEADER_COLOR: [u8; 3] = [0x9a, 0x9a, 0xa4];
@@ -33,25 +34,105 @@ const HEADER_COLOR: [u8; 3] = [0x9a, 0x9a, 0xa4];
const DEPTH_RANGE: (f32, f32) = (0.0, 0.6);
const WIDTH_RANGE: (f32, f32) = (2.0, 24.0);
+/// Sample count for the spec written to config — enough that the 32-slot
+/// renderer LUT sees the curve, few enough that the config line stays sane.
+const SPEC_SAMPLES: usize = 17;
+
#[derive(Debug, Clone)]
enum BevelMsg {
Exit,
}
+/// One profile section's shape state: the three knob sliders plus whether
+/// the profile has diverged from the analytic default.
+struct ProfileKnobs {
+ shoulder: Adapted<Slider>,
+ base: Adapted<Slider>,
+ bias: Adapted<Slider>,
+ /// False until a knob moves (or config carried saved knobs): the DE
+ /// renders its analytic profile and Save writes the identity sentinel.
+ custom: bool,
+ /// Last spec applied+logged.
+ last_spec: String,
+}
+
+impl ProfileKnobs {
+ fn new(seed: Option<(f32, f32, f32)>) -> Self {
+ let (s, b, c) = seed.unwrap_or((0.5, 0.5, 0.5));
+ let knob = |v: f32, label: &str| {
+ Slider::new().with_label(label).with_value(v.clamp(0.0, 1.0)).with_scroll(true)
+ };
+ let mut this = Self {
+ shoulder: knob(s, "Shoulder"),
+ base: knob(b, "Base"),
+ bias: knob(c, "Bias"),
+ custom: seed.is_some(),
+ last_spec: String::new(),
+ };
+ this.last_spec = if this.custom { this.spec() } else { IDENTITY_SPEC.to_string() };
+ this
+ }
+
+ fn values(&self) -> (f32, f32, f32) {
+ (self.shoulder.inner().value(), self.base.inner().value(), self.bias.inner().value())
+ }
+
+ /// The section's height curve `h(v)`: bias pre-warp, then the rational
+ /// two-exponent ease. Exponents run 0.5 (sharp crease) → 2 (smoothstep,
+ /// the midpoint) → 8 (wide round-over); bias skews the drop early/late.
+ fn eval(&self, v: f32) -> f32 {
+ let (s, b, c) = self.values();
+ let a = 2.0 * 4f32.powf(2.0 * s - 1.0);
+ let be = 2.0 * 4f32.powf(2.0 * b - 1.0);
+ let g = 4f32.powf(2.0 * c - 1.0);
+ let w = v.clamp(0.0, 1.0).powf(g);
+ let num = w.powf(a);
+ let den = num + (1.0 - w).powf(be);
+ if den <= f32::EPSILON {
+ return if w > 0.5 { 1.0 } else { 0.0 };
+ }
+ (num / den).clamp(0.0, 1.0)
+ }
+
+ /// The curve sampled as linear ramp-spec keys — what the renderer LUT
+ /// and the config carry.
+ fn keys(&self) -> Vec<(f32, f32)> {
+ (0..SPEC_SAMPLES)
+ .map(|i| {
+ let v = i as f32 / (SPEC_SAMPLES - 1) as f32;
+ (v, self.eval(v))
+ })
+ .collect()
+ }
+
+ fn spec(&self) -> String {
+ cce_ui::widget::format_ramp_spec(&self.keys(), false)
+ }
+
+ fn take_change(&mut self) -> bool {
+ // Bitwise-or on purpose: every slider's flag must drain.
+ self.shoulder.take_change() | self.base.take_change() | self.bias.take_change()
+ }
+
+ fn set_defaults(&mut self) {
+ self.shoulder.set_value(0.5);
+ self.base.set_value(0.5);
+ self.bias.set_value(0.5);
+ self.custom = false;
+ self.last_spec = IDENTITY_SPEC.to_string();
+ }
+}
+
struct BevelPopup {
- /// The carve wall curve — what `carve_slope` renders on every
+ /// The carve wall — what `carve_slope` renders on every
/// recess/boss/ridge in the DE.
- wall_ramp: Adapted<Ramp>,
- /// The plate perimeter roll curve — `roll_slope`'s descent profile.
- edge_ramp: Adapted<Ramp>,
+ wall: ProfileKnobs,
+ /// The plate perimeter roll — `roll_slope`'s descent profile.
+ edge: ProfileKnobs,
depth_slider: Adapted<Slider>,
width_slider: Adapted<Slider>,
save_button: Adapted<Button>,
reset_button: Adapted<Button>,
- /// Last specs applied+logged — edits are detected by comparison, so
- /// tick-driven changes (hover-scroll glides) apply too.
- last_wall: String,
- last_edge: String,
/// Status line under the buttons: what the last save/reset did.
status: String,
ui_context: cce_ui::context::UiContext,
@@ -63,13 +144,118 @@ struct BevelPopup {
wall_header: (f32, f32),
edge_header: (f32, f32),
status_pos: (f32, f32),
+ wall_rect: Rect,
+ edge_rect: Rect,
+}
+
+/// Parse a saved "shoulder,base,bias" knob triple.
+fn parse_knobs(s: &str) -> Option<(f32, f32, f32)> {
+ let mut it = s.split(',').map(|p| p.trim().parse::<f32>());
+ match (it.next(), it.next(), it.next()) {
+ (Some(Ok(a)), Some(Ok(b)), Some(Ok(c))) => {
+ Some((a.clamp(0.0, 1.0), b.clamp(0.0, 1.0), c.clamp(0.0, 1.0)))
+ }
+ _ => None,
+ }
+}
+
+/// Draw one profile as a lit cutaway: the material slab (plate color) inside
+/// a dark opening, its surface stroked with segment lighting from the DE's
+/// light azimuth. `has_floor` distinguishes the carve (wall meets a floor
+/// inside the material) from the roll (the surface drops to the silhouette
+/// and the material simply ends — air beyond the edge).
+fn draw_section(pc: &mut PaintCtx, rect: Rect, profile: &ProfileKnobs, has_floor: bool) {
+ let radius = 6.0f32;
+ let radii = (radius, radius, radius, radius);
+ pc.rounded_rect(rect, radius, (true, true, true, true), [0.08, 0.08, 0.10, 1.0]);
+
+ // The section geometry: plateau band, then the wall over `wall_w`, then
+ // (carve only) the floor band. Vertical span is the feature depth.
+ let m = 12.0f32;
+ let y_top = rect.y + 16.0;
+ let y_bot = rect.y + rect.height - 22.0;
+ let drop = y_bot - y_top;
+ let x_l = rect.x + m;
+ let x_r = rect.x + rect.width - m;
+ let plateau_w = (x_r - x_l) * 0.16;
+ let wall_w = (x_r - x_l) * if has_floor { 0.56 } else { 0.68 };
+ let x0 = x_l + plateau_w;
+ let x1 = x0 + wall_w;
+
+ // Surface height at a section x.
+ let surface_y = |x: f32| -> Option<f32> {
+ if x <= x0 {
+ Some(y_top)
+ } else if x <= x1 {
+ Some(y_top + profile.eval((x - x0) / wall_w) * drop)
+ } else if has_floor {
+ Some(y_bot)
+ } else {
+ None // past the silhouette: air
+ }
+ };
+
+ // The slab: the plate material itself, filled from the surface down to
+ // the cut's bottom edge. Columns share exact edges (opaque fill, but the
+ // ramp-fill rule keeps seams clean under AA).
+ let mut slab = cce_ui::color::page_low_color();
+ slab = [slab[0] * 1.25 + 0.03, slab[1] * 1.25 + 0.03, slab[2] * 1.25 + 0.03, 1.0];
+ let slab_bot = rect.y + rect.height - 10.0;
+ let step = 2.0f32;
+ let mut x = x_l;
+ while x < x_r {
+ let xm = (x + step / 2.0).min(x_r);
+ if let Some(sy) = surface_y(xm) {
+ let w = step.min(x_r - x);
+ pc.quad(Rect { x, y: sy, width: w, height: (slab_bot - sy).max(0.0) }, slab);
+ }
+ x += step;
+ }
+
+ // The surface stroke, lit per segment: outward normal (material below)
+ // against the DE light azimuth — the same light the real walls shade by.
+ let az = cce_ui::layout::light_source_position();
+ let (lx, ly) = (az.cos(), -az.sin());
+ let base = [0.60f32, 0.65, 0.74];
+ let n_seg = 56usize;
+ let seg_end = if has_floor { x_r } else { x1 };
+ let mut prev = (x_l, surface_y(x_l).unwrap_or(y_top));
+ for i in 1..=n_seg {
+ let x = x_l + (seg_end - x_l) * i as f32 / n_seg as f32;
+ let Some(y) = surface_y(x) else { break };
+ let (dx, dy) = (x - prev.0, y - prev.1);
+ let len = (dx * dx + dy * dy).sqrt().max(1e-3);
+ let (nx, ny) = (dy / len, -dx / len);
+ let lit = (nx * lx + ny * ly) * 0.35;
+ let c = [
+ (base[0] + lit).clamp(0.0, 1.0),
+ (base[1] + lit).clamp(0.0, 1.0),
+ (base[2] + lit).clamp(0.0, 1.0),
+ 1.0,
+ ];
+ pc.vector(prev.0, prev.1, x, y, 2.5, c, Cap::Round);
+ prev = (x, y);
+ }
+ // The roll's cut face: a dimmer vertical edge closing the slab at the
+ // silhouette.
+ if !has_floor {
+ pc.vector(x1, y_bot, x1, slab_bot, 2.0, [0.36, 0.39, 0.46, 1.0], Cap::Round);
+ }
+
+ // The opening's rim, drawn last so its shading falls over the slab edges.
+ let depth = cce_ui::layout::bevel_width().min(rect.height * 0.2);
+ pc.recess(rect, radii, depth);
}
impl BevelPopup {
- fn root_ids(&self) -> [WidgetId; 6] {
+ fn root_ids(&self) -> [WidgetId; 10] {
[
- self.wall_ramp.id(),
- self.edge_ramp.id(),
+ self.wall.shoulder.id(),
+ self.wall.base.id(),
+ self.wall.bias.id(),
+ self.edge.shoulder.id(),
+ self.edge.base.id(),
+ self.edge.bias.id(),
self.depth_slider.id(),
self.width_slider.id(),
self.save_button.id(),
@@ -77,10 +263,14 @@ impl BevelPopup {
]
}
- fn roots(&mut self) -> [*mut (dyn WidgetHost + 'static); 6] {
+ fn roots(&mut self) -> [*mut (dyn WidgetHost + 'static); 10] {
[
- self.wall_ramp.as_ptr_mut(),
- self.edge_ramp.as_ptr_mut(),
+ self.wall.shoulder.as_ptr_mut(),
+ self.wall.base.as_ptr_mut(),
+ self.wall.bias.as_ptr_mut(),
+ self.edge.shoulder.as_ptr_mut(),
+ self.edge.base.as_ptr_mut(),
+ self.edge.bias.as_ptr_mut(),
self.depth_slider.as_ptr_mut(),
self.width_slider.as_ptr_mut(),
self.save_button.as_ptr_mut(),
@@ -88,35 +278,25 @@ impl BevelPopup {
]
}
- /// Install a ramp's curve as the live wall (or roll) profile. The
- /// identity-smooth spec is the "analytic" sentinel ([`IDENTITY_SPEC`]):
- /// it clears back to the built-in profile instead of installing.
- fn apply_profile(ramp: &Ramp, spec: &str, roll: bool) {
- let keys: Vec<(f32, f32)> = ramp.keys.iter().map(|k| (k.pos, k.value)).collect();
- let smooth = ramp.smooth();
- match (spec == IDENTITY_SPEC, roll) {
- (true, true) => cce_ui::layout::clear_roll_profile(),
- (true, false) => cce_ui::layout::clear_bevel_profile(),
- (false, true) => cce_ui::layout::set_roll_profile_keys(&keys, smooth),
- (false, false) => cce_ui::layout::set_bevel_profile_keys(&keys, smooth),
- }
- }
-
/// `take_*` plumbing after any routed dispatch — state-gated, so it does
/// not matter which propagate call consumed the event.
fn drain_widget_changes(&mut self) {
- let wall_spec = self.wall_ramp.inner().spec_string();
- if wall_spec != self.last_wall {
- Self::apply_profile(self.wall_ramp.inner(), &wall_spec, false);
- println!("wall {wall_spec}");
- self.last_wall = wall_spec;
+ if self.wall.take_change() {
+ self.wall.custom = true;
+ let keys = self.wall.keys();
+ cce_ui::layout::set_bevel_profile_keys(&keys, false);
+ let spec = self.wall.spec();
+ println!("wall {spec}");
+ self.wall.last_spec = spec;
self.needs_rebuild = true;
}
- let edge_spec = self.edge_ramp.inner().spec_string();
- if edge_spec != self.last_edge {
- Self::apply_profile(self.edge_ramp.inner(), &edge_spec, true);
- println!("edge {edge_spec}");
- self.last_edge = edge_spec;
+ if self.edge.take_change() {
+ self.edge.custom = true;
+ let keys = self.edge.keys();
+ cce_ui::layout::set_roll_profile_keys(&keys, false);
+ let spec = self.edge.spec();
+ println!("edge {spec}");
+ self.edge.last_spec = spec;
self.needs_rebuild = true;
}
if self.depth_slider.take_change() {
@@ -147,21 +327,26 @@ impl BevelPopup {
/// Persist the current material to the shared config
/// (`style.surface.relief` — the same keys every app reads at startup).
- /// Identity specs are written as-is; the loader reads them as analytic.
+ /// Untouched sections write the identity sentinel (= analytic); the knob
+ /// triples ride along so this editor reopens where you left it.
fn save_to_config(&mut self) {
let path = cce_ui::config::get_config_path();
let p = path.to_string_lossy().into_owned();
let depth = format!("{:.3}", self.depth_slider.inner().get_scaled_value());
let width = format!("{:.2}", self.width_slider.inner().get_scaled_value());
- let ok = cce_ui::config::write_config_value(&p, "style.surface.relief.depth", &depth, "style")
- & cce_ui::config::write_config_value(&p, "style.surface.relief.width", &width, "style")
- & cce_ui::config::write_config_value(&p, "style.surface.relief.profile", &self.last_wall, "style")
- & cce_ui::config::write_config_value(
- &p,
- "style.surface.relief.edge_profile",
- &self.last_edge,
- "style",
- );
+ let knob_str = |k: &ProfileKnobs| {
+ let (s, b, c) = k.values();
+ format!("{s:.3},{b:.3},{c:.3}")
+ };
+ let w = &mut |key: &str, value: &str| {
+ cce_ui::config::write_config_value(&p, key, value, "style")
+ };
+ let ok = w("style.surface.relief.depth", &depth)
+ & w("style.surface.relief.width", &width)
+ & w("style.surface.relief.profile", &self.wall.last_spec)
+ & w("style.surface.relief.edge_profile", &self.edge.last_spec)
+ & w("style.surface.relief.profile_knobs", &knob_str(&self.wall))
+ & w("style.surface.relief.edge_knobs", &knob_str(&self.edge));
self.status = if ok {
println!("saved {p}");
"Saved — apps pick the material up on start.".to_string()
@@ -170,12 +355,13 @@ impl BevelPopup {
};
}
- /// Back to the analytic material, live only (Save persists it): identity
- /// curves on both ramps, default depth/width.
+ /// Back to the analytic material, live only (Save persists it): knobs to
+ /// their midpoints, both profiles cleared, default depth/width.
fn reset_live(&mut self) {
- self.wall_ramp.set_spec(IDENTITY_SPEC);
- self.edge_ramp.set_spec(IDENTITY_SPEC);
- // drain_widget_changes sees the spec change and clears the profiles.
+ self.wall.set_defaults();
+ self.edge.set_defaults();
+ cce_ui::layout::clear_bevel_profile();
+ cce_ui::layout::clear_roll_profile();
if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
reg.set_float("bevel_depth", 0.15);
reg.set_float("bevel_width", 9.3);
@@ -197,34 +383,25 @@ impl Application for BevelPopup {
_sender: calloop::channel::Sender<Self::Message>,
) -> Self {
cce_ui::scale::set_scale_factor(1.0);
- // Force the lazy config load BEFORE reading the registry: the specs
- // are read directly (no getter wraps them), so nothing else has
- // triggered it yet this early in startup.
+ // Force the lazy config load BEFORE reading the registry: the knob
+ // strings are read directly (no getter wraps them), so nothing else
+ // has triggered it yet this early in startup.
cce_ui::layout::lazy_init_style_registry();
- // Seed the ramps from the configured material (reload_config has
- // already installed the live profiles); identity when unconfigured.
- let reg = cce_ui::layout::get_style_registry();
- let (wall_spec, edge_spec) = {
- let reg = reg.read().unwrap();
+ let (wall_seed, edge_seed) = {
+ let reg = cce_ui::layout::get_style_registry().read().unwrap();
(
- reg.get_string("bevel_profile_spec").unwrap_or_else(|| IDENTITY_SPEC.to_string()),
- reg.get_string("roll_profile_spec").unwrap_or_else(|| IDENTITY_SPEC.to_string()),
+ reg.get_string("bevel_profile_knobs").as_deref().and_then(parse_knobs),
+ reg.get_string("roll_profile_knobs").as_deref().and_then(parse_knobs),
)
};
- let mut wall_ramp = Ramp::new();
- wall_ramp.set_spec(&wall_spec);
- let mut edge_ramp = Ramp::new();
- edge_ramp.set_spec(&edge_spec);
- let last_wall = wall_ramp.inner().spec_string();
- let last_edge = edge_ramp.inner().spec_string();
let depth = cce_ui::layout::bevel_depth();
let width = cce_ui::layout::bevel_width();
let (dmin, dmax) = DEPTH_RANGE;
let (wmin, wmax) = WIDTH_RANGE;
Self {
- wall_ramp,
- edge_ramp,
+ wall: ProfileKnobs::new(wall_seed),
+ edge: ProfileKnobs::new(edge_seed),
depth_slider: Slider::new()
.with_label("Depth")
.with_range(dmin, dmax)
@@ -241,35 +418,29 @@ impl Application for BevelPopup {
.with_scroll(true),
save_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Save"),
reset_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Reset"),
- last_wall,
- last_edge,
status: "Edits apply live; Save writes config.kdl.".to_string(),
ui_context: cce_ui::context::UiContext::new(),
- width: 620,
- height: 1020,
+ width: 520,
+ height: 620,
scale_factor: 1.0,
needs_rebuild: true,
registered: false,
wall_header: (0.0, 0.0),
edge_header: (0.0, 0.0),
status_pos: (0.0, 0.0),
+ wall_rect: Rect::ZERO,
+ edge_rect: Rect::ZERO,
}
}
- // Buffer-larger-than-geometry mode (the cce-ramp idiom): key pegs painted
- // on the rim render outside the window frame; rim clicks fall through.
- fn overflow_margin(&self) -> u32 {
- OVERFLOW_MARGIN as u32
- }
-
fn settings(&self) -> WindowSettings {
WindowSettings {
title: "Bevel".to_string(),
app_id: "cce-bevel".to_string(),
- width: 540,
- height: 940,
+ width: 520,
+ height: 620,
fullscreen: false,
- min_size: Some((460, 760)),
+ min_size: Some((440, 540)),
}
}
@@ -281,7 +452,6 @@ impl Application for BevelPopup {
fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
if self.ui_context.tick(dt) {
- // Tick-driven edits (hover-scroll glide) apply and log too.
self.drain_widget_changes();
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -309,28 +479,44 @@ impl Application for BevelPopup {
self.scale_factor = scale;
cce_ui::scale::set_scale_factor(scale as f32);
- // Manual column layout at the popup pad (the cce-ramp idiom).
- let pad = OVERFLOW_MARGIN + cce_ui::layout::backplate_padding() / 2.0;
+ // Manual column layout: per section a header, the cutaway, and
+ // the three knobs on one row; then the global rows.
+ let pad = cce_ui::layout::backplate_padding();
let x = pad;
let w = (self.width as f32 - 2.0 * pad).max(0.0);
let header_h = HEADER_FONT_SIZE + 7.0;
- let gap = 16.0;
- let knob_h = 22.0 + Ramp::label_strip();
+ let gap = 14.0;
+ let strip = {
+ let (_, fsize) = cce_ui::layout::control_label_font_detached_parsed();
+ fsize + cce_ui::layout::control_label_margin()
+ };
+ let knob_h = 22.0 + strip;
let button_h = 26.0;
let status_h = HEADER_FONT_SIZE + 4.0;
- let fixed = 2.0 * header_h + 3.0 * gap + knob_h + gap + button_h + status_h + 8.0;
- let ramp_h =
- ((self.height as f32 - 2.0 * pad - fixed) / 2.0).max(220.0);
+ let fixed = 2.0 * (header_h + gap + knob_h + gap) + knob_h + gap + button_h + 8.0
+ + status_h;
+ let cut_h = ((self.height as f32 - 2.0 * pad - fixed) / 2.0).clamp(90.0, 200.0);
+
+ let kw = (w - 2.0 * gap) / 3.0;
+ let knob_row = |k: &mut ProfileKnobs, x: f32, y: f32| {
+ k.shoulder.set_rect(x, y, kw, knob_h);
+ k.base.set_rect(x + kw + gap, y, kw, knob_h);
+ k.bias.set_rect(x + 2.0 * (kw + gap), y, kw, knob_h);
+ };
let mut y = pad;
self.wall_header = (x, y);
y += header_h;
- self.wall_ramp.set_rect(x, y, w, ramp_h);
- y += ramp_h + gap;
+ self.wall_rect = Rect { x, y, width: w, height: cut_h };
+ y += cut_h + gap;
+ knob_row(&mut self.wall, x, y);
+ y += knob_h + gap;
self.edge_header = (x, y);
y += header_h;
- self.edge_ramp.set_rect(x, y, w, ramp_h);
- y += ramp_h + gap;
+ self.edge_rect = Rect { x, y, width: w, height: cut_h };
+ y += cut_h + gap;
+ knob_row(&mut self.edge, x, y);
+ y += knob_h + gap;
let half = (w - gap) / 2.0;
self.depth_slider.set_rect(x, y, half, knob_h);
self.width_slider.set_rect(x + half + gap, y, half, knob_h);
@@ -344,29 +530,20 @@ impl Application for BevelPopup {
self.ui_context.rebuild_spatial_grid();
}
- self.ui_context.clear_popovers();
- if self.wall_ramp.popover_rect().is_some() {
- self.ui_context.register_popover(&mut self.wall_ramp);
- }
- if self.edge_ramp.popover_rect().is_some() {
- self.ui_context.register_popover(&mut self.edge_ramp);
- }
-
let mut pc = PaintCtx::new();
let (w, h) = (self.width as f32, self.height as f32);
- // The window plate at FULL opacity on purpose: its rolled perimeter is
- // the edge profile's preview, and the wells/buttons below preview the
- // wall profile — the popup is its own material sample.
+ // The window plate at full opacity on purpose: its rolled perimeter
+ // previews the edge profile, and the wells/buttons preview the wall
+ // profile — the popup is its own material sample.
let mut plate = cce_ui::color::page_low_color();
if plate[3] > 0.001 {
plate[3] = cce_ui::color::active_backplate_opacity();
}
let radius = cce_ui::colors::backplate_corner_radius();
let bevel = cce_ui::layout::bevel_width();
- let m = OVERFLOW_MARGIN;
pc.plate(
- Rect { x: m, y: m, width: w - 2.0 * m, height: h - 2.0 * m },
+ Rect { x: 0.0, y: 0.0, width: w, height: h },
(radius, radius, radius, radius),
plate,
bevel,
@@ -387,34 +564,25 @@ impl Application for BevelPopup {
header(&mut pc, "Edge profile — plate perimeter roll", self.edge_header);
header(&mut pc, &self.status, self.status_pos);
- cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.wall_ramp, &mut pc);
- cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.edge_ramp, &mut pc);
- cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.depth_slider, &mut pc);
- cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.width_slider, &mut pc);
+ draw_section(&mut pc, self.wall_rect, &self.wall, true);
+ draw_section(&mut pc, self.edge_rect, &self.edge, false);
+
+ for s in [
+ &self.wall.shoulder,
+ &self.wall.base,
+ &self.wall.bias,
+ &self.edge.shoulder,
+ &self.edge.base,
+ &self.edge.bias,
+ &self.depth_slider,
+ &self.width_slider,
+ ] {
+ cce_ui::scene::painter::paint_root_into(&self.ui_context, s, &mut pc);
+ }
cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.save_button, &mut pc);
cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.reset_button, &mut pc);
- // The ramps' field-dropdown popovers, drawn into the frame on top.
- for ramp in [&self.wall_ramp, &self.edge_ramp] {
- let Some((px, py, pw, ph)) = ramp.popover_rect() else { continue };
- let mut coll = cce_ui::layout::PopoverCollector::new();
- ramp.inner().preset_dropdown.render_popover(&mut coll);
- ramp.inner().line_type_dropdown.render_popover(&mut coll);
- for &(c, x, y, qw, qh) in &coll.rects {
- pc.quad(Rect { x, y, width: qw, height: qh }, c);
- }
- let bounds = Some([px, py, px + pw, py + ph]);
- for (content, size, tx, ty, color, font, _b) in coll.texts {
- let color_u8 = [
- (color[0] * 255.0).clamp(0.0, 255.0) as u8,
- (color[1] * 255.0).clamp(0.0, 255.0) as u8,
- (color[2] * 255.0).clamp(0.0, 255.0) as u8,
- ];
- pc.text_with(content, tx, ty, size, color_u8, font, bounds);
- }
- }
-
- // The shared context menu, last, on top of everything.
+ // The shared context menu (slider Copy/Paste), last, on top.
if self.ui_context.is_context_menu_visible() {
for (qx, qy, qw, qh, c) in self.ui_context.context_menu_quads() {
pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, c);
diff --git a/src/layout.rs b/src/layout.rs
index 2d22ee5..55171ba 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -123,6 +123,10 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
// (written by cce-bevel, installed by reload_config).
"style.surface.relief.profile" => "bevel_profile_spec",
"style.surface.relief.edge_profile" => "roll_profile_spec",
+ // cce-bevel's slider positions behind those specs
+ // ("shoulder,base,bias" — only the editor reads these).
+ "style.surface.relief.profile_knobs" => "bevel_profile_knobs",
+ "style.surface.relief.edge_knobs" => "roll_profile_knobs",
"style.container.section.depth" => "section_depth",
"window_manager.bevel_shader" => "bevel_shader",
"window_manager.control_relief" => "control_relief",