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