git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/input/bevel_preview.rs (7.7K)

  1 //! `BevelPreview` — the `(bevel)` config type's inline preview: a miniature
  2 //! lit cross-section of a relief profile (plateau → wall → floor), shaped by
  3 //! a "shoulder,base,bias" knob triple. Read-only: it exists to SHOW the
  4 //! current material and take a click, which hosts (cce-data-editor) answer
  5 //! by opening the full `cce-relief` editor. The curve family and knob
  6 //! semantics are cce-relief's — see [`bevel_ease`].
  7 
  8 use crate::scene::layout::Rect;
  9 use crate::scene::paint::{Cap, PaintCtx};
 10 use crate::widget::{Adapted, ElementState, Event, EventCtx, Input, Layout, MouseButton, Paint};
 11 
 12 /// The relief profile curve family shared by `cce-relief` and this preview:
 13 /// the two-exponent rational ease `h(w) = w^a / (w^a + (1-w)^b)` over a bias
 14 /// pre-warp `w = v^g`. Monotone and endpoint-exact; slider midpoints
 15 /// (0.5, 0.5, 0.5) give a=b=2, g=1 — the analytic smoothstep. Exponents run
 16 /// 0.5 (sharp crease) → 2 → 8 (wide round-over).
 17 pub fn bevel_ease(shoulder: f32, base: f32, bias: f32, v: f32) -> f32 {
 18     let a = 2.0 * 4f32.powf(2.0 * shoulder - 1.0);
 19     let be = 2.0 * 4f32.powf(2.0 * base - 1.0);
 20     let g = 4f32.powf(2.0 * bias - 1.0);
 21     let w = v.clamp(0.0, 1.0).powf(g);
 22     let num = w.powf(a);
 23     let den = num + (1.0 - w).powf(be);
 24     if den <= f32::EPSILON {
 25         return if w > 0.5 { 1.0 } else { 0.0 };
 26     }
 27     (num / den).clamp(0.0, 1.0)
 28 }
 29 
 30 /// Parse a "shoulder,base,bias" knob triple (the `(bevel)` value format: a
 31 /// `(relief)` value's `k=` ride-along, and what cce-relief keeps per target
 32 /// in its own state.kdl).
 33 pub fn parse_bevel_knobs(s: &str) -> Option<(f32, f32, f32)> {
 34     let mut it = s.split(',').map(|p| p.trim().parse::<f32>());
 35     match (it.next(), it.next(), it.next()) {
 36         (Some(Ok(a)), Some(Ok(b)), Some(Ok(c))) => {
 37             Some((a.clamp(0.0, 1.0), b.clamp(0.0, 1.0), c.clamp(0.0, 1.0)))
 38         }
 39         _ => None,
 40     }
 41 }
 42 
 43 pub struct BevelPreview {
 44     knobs: (f32, f32, f32),
 45     just_clicked: bool,
 46     hovered: bool,
 47 }
 48 
 49 impl BevelPreview {
 50     pub fn new() -> Adapted<BevelPreview> {
 51         Adapted::new(BevelPreview {
 52             knobs: (0.5, 0.5, 0.5),
 53             just_clicked: false,
 54             hovered: false,
 55         })
 56     }
 57 
 58     /// Set the previewed profile from a knob-triple value string; anything
 59     /// unparsable falls back to the analytic midpoints.
 60     pub fn set_knobs_str(&mut self, s: &str) {
 61         self.knobs = parse_bevel_knobs(s).unwrap_or((0.5, 0.5, 0.5));
 62     }
 63 
 64     pub fn knobs(&self) -> (f32, f32, f32) {
 65         self.knobs
 66     }
 67 }
 68 
 69 impl Layout for BevelPreview {}
 70 
 71 impl Paint for BevelPreview {
 72     fn color(&self) -> [f32; 4] {
 73         // The well bg is emitted in `paint` (hover-dependent); no base fill.
 74         [0.0, 0.0, 0.0, 0.0]
 75     }
 76 
 77     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 78         // The opening: the shared canvas well (`PaintCtx::well_floor`), its floor
 79         // lifted on hover (the click cue); the rim is drawn last, over the content.
 80         let radius = crate::layout::textbox_corner_radius();
 81         ctx.well_floor(rect, radius, &crate::scene::Material::pane(), self.hovered);
 82 
 83         // Mini cutaway: plateau band, the wall over a square-ish domain, then
 84         // the floor — cce-relief's `draw_section` reduced to swatch scale.
 85         let m = 3.0f32;
 86         let x_l = rect.x + m;
 87         let x_r = rect.x + rect.width - m;
 88         let y_top = rect.y + m + 2.0;
 89         let drop = (rect.height - 2.0 * m - 6.0).max(4.0);
 90         let y_bot = y_top + drop;
 91         let avail = x_r - x_l;
 92         let wall_w = drop.min(avail * 0.5);
 93         let plateau_w = (avail - wall_w) * 0.45;
 94         let (x0, x1) = (x_l + plateau_w, x_l + plateau_w + wall_w);
 95 
 96         let (s, b, c) = self.knobs;
 97         let surface_y = |x: f32| -> f32 {
 98             if x <= x0 {
 99                 y_top
100             } else if x <= x1 {
101                 y_top + bevel_ease(s, b, c, (x - x0) / wall_w) * drop
102             } else {
103                 y_bot
104             }
105         };
106 
107         // The slab under the surface, in the plate material's tone.
108         let mut slab = crate::color::page_low_color();
109         slab = [slab[0] * 1.25 + 0.03, slab[1] * 1.25 + 0.03, slab[2] * 1.25 + 0.03, 1.0];
110         let slab_bot = rect.y + rect.height - m;
111         let step = 2.0f32;
112         let mut x = x_l;
113         while x < x_r {
114             let sy = surface_y((x + step / 2.0).min(x_r));
115             let w = step.min(x_r - x);
116             ctx.quad(Rect { x, y: sy, width: w, height: (slab_bot - sy).max(0.0) }, slab);
117             x += step;
118         }
119 
120         // The surface stroke, lit per segment under the DE light azimuth —
121         // the same shading the real walls answer to.
122         let az = crate::layout::light_source_position();
123         let (lx, ly) = (az.cos(), -az.sin());
124         let base_c = [0.60f32, 0.65, 0.74];
125         let n_seg = 24usize;
126         let mut prev = (x_l, surface_y(x_l));
127         for i in 1..=n_seg {
128             let x = x_l + (x_r - x_l) * i as f32 / n_seg as f32;
129             let y = surface_y(x);
130             let (dx, dy) = (x - prev.0, y - prev.1);
131             let len = (dx * dx + dy * dy).sqrt().max(1e-3);
132             let (nx, ny) = (dy / len, -dx / len);
133             let lit = (nx * lx + ny * ly) * 0.35;
134             let col = [
135                 (base_c[0] + lit).clamp(0.0, 1.0),
136                 (base_c[1] + lit).clamp(0.0, 1.0),
137                 (base_c[2] + lit).clamp(0.0, 1.0),
138                 1.0,
139             ];
140             ctx.vector(prev.0, prev.1, x, y, 1.5, col, Cap::Round);
141             prev = (x, y);
142         }
143 
144         ctx.well_rim(rect, radius, crate::layout::control_relief());
145     }
146 }
147 
148 impl Input for BevelPreview {
149     fn on_event(&mut self, event: &Event, _ectx: &mut EventCtx) -> bool {
150         match event {
151             Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
152                 self.just_clicked = true;
153                 true
154             }
155             Event::MouseEnter => {
156                 self.hovered = true;
157                 false
158             }
159             Event::MouseLeave => {
160                 self.hovered = false;
161                 false
162             }
163             _ => false,
164         }
165     }
166 
167     fn take_click(&mut self) -> bool {
168         std::mem::take(&mut self.just_clicked)
169     }
170 }
171 
172 #[cfg(test)]
173 mod tests {
174     use super::*;
175     use crate::widget::{UiContext, WidgetHost};
176 
177     #[test]
178     fn ease_is_monotone_and_endpoint_exact() {
179         for &(s, b, c) in &[(0.5, 0.5, 0.5), (0.0, 1.0, 0.3), (0.9, 0.1, 0.8)] {
180             assert!(bevel_ease(s, b, c, 0.0).abs() < 1e-4);
181             assert!((bevel_ease(s, b, c, 1.0) - 1.0).abs() < 1e-4);
182             let mut last = -1.0f32;
183             for i in 0..=32 {
184                 let h = bevel_ease(s, b, c, i as f32 / 32.0);
185                 assert!(h >= last - 1e-4, "monotone at ({s},{b},{c})");
186                 last = h;
187             }
188         }
189         // Midpoints are the analytic smoothstep.
190         let mid = bevel_ease(0.5, 0.5, 0.5, 0.5);
191         assert!((mid - 0.5).abs() < 1e-4);
192     }
193 
194     #[test]
195     fn knob_parse_clamps_and_rejects() {
196         assert_eq!(parse_bevel_knobs("0.2, 0.7, 1.5"), Some((0.2, 0.7, 1.0)));
197         assert_eq!(parse_bevel_knobs("0.2,0.7"), None);
198         assert_eq!(parse_bevel_knobs("junk"), None);
199     }
200 
201     #[test]
202     fn click_reports_once() {
203         let mut ctx = UiContext::new();
204         let p = ctx.insert(BevelPreview::new());
205         let id = p.id();
206         WidgetHost::set_rect(&mut ctx[p], 0.0, 0.0, 125.0, 26.0);
207         let ev = Event::MouseButton {
208             button: MouseButton::Left,
209             state: ElementState::Pressed,
210             x: 10.0,
211             y: 10.0,
212             local_x: 10.0,
213             local_y: 10.0,
214         };
215         assert!(ctx.propagate_event(&ev, id));
216         assert!(ctx[p].take_click());
217         assert!(!ctx[p].take_click());
218     }
219 }