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 }