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 }