GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/input/ramp_preview.rs (5.1K)
1 //! `RampPreview` — the `(ramp)` config type's inline preview: the spec's
2 //! value curve drawn as a lit polyline in a dark well. Read-only, the
3 //! sibling of [`super::bevel_preview::BevelPreview`]: it exists to SHOW the
4 //! curve and take a click, which hosts (cce-data-editor) answer by opening
5 //! the full `cce-ramp` editor on the key. The spec format is the DE-wide
6 //! ramp string (`"smooth;0.000:0.150,0.400:1.000,…"` — see
7 //! [`super::ramp::parse_ramp_spec`]).
8
9 use crate::scene::layout::Rect;
10 use crate::scene::paint::{Cap, PaintCtx};
11 use crate::widget::{Adapted, ElementState, Event, EventCtx, Input, Layout, MouseButton, Paint};
12
13 pub struct RampPreview {
14 /// Parsed spec: sorted `(pos, value)` keys + smooth/linear blending.
15 keys: Vec<(f32, f32)>,
16 smooth: bool,
17 just_clicked: bool,
18 hovered: bool,
19 }
20
21 impl RampPreview {
22 pub fn new() -> Adapted<RampPreview> {
23 Adapted::new(RampPreview {
24 keys: vec![(0.0, 0.0), (1.0, 1.0)],
25 smooth: false,
26 just_clicked: false,
27 hovered: false,
28 })
29 }
30
31 /// Set the previewed curve from a ramp spec string; anything unparsable
32 /// falls back to the linear identity.
33 pub fn set_spec_str(&mut self, s: &str) {
34 match crate::widget::parse_ramp_spec(s) {
35 Some((keys, smooth)) => {
36 self.keys = keys;
37 self.smooth = smooth;
38 }
39 None => {
40 self.keys = vec![(0.0, 0.0), (1.0, 1.0)];
41 self.smooth = false;
42 }
43 }
44 }
45
46 /// The curve's value at `t` — [`crate::layout::sample_ramp_keys`], the
47 /// same interpolation `Ramp` draws and every consumer evaluates.
48 fn value_at(&self, t: f32) -> f32 {
49 crate::layout::sample_ramp_keys(&self.keys, self.smooth, t)
50 }
51 }
52
53 impl Layout for RampPreview {}
54
55 impl Paint for RampPreview {
56 fn color(&self) -> [f32; 4] {
57 // The well bg is emitted in `paint` (hover-dependent); no base fill.
58 [0.0, 0.0, 0.0, 0.0]
59 }
60
61 fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
62 // The opening: the shared canvas well (`PaintCtx::well_floor`), its floor
63 // lifted on hover (the click cue); the rim is drawn last, over the content.
64 let radius = crate::layout::textbox_corner_radius();
65 ctx.well_floor(rect, radius, &crate::scene::Material::pane(), self.hovered);
66
67 let m = 4.0f32;
68 let x_l = rect.x + m;
69 let x_r = rect.x + rect.width - m;
70 let y_hi = rect.y + m;
71 let y_lo = rect.y + rect.height - m;
72 if x_r <= x_l || y_lo <= y_hi {
73 ctx.well_rim(rect, radius, crate::layout::control_relief());
74 return;
75 }
76 let y_of = |v: f32| y_lo - v.clamp(0.0, 1.0) * (y_lo - y_hi);
77
78 // The curve, lit per segment under the DE light azimuth — the same
79 // treatment as BevelPreview's surface stroke, so the two types read
80 // as siblings in a key list.
81 let az = crate::layout::light_source_position();
82 let (lx, ly) = (az.cos(), -az.sin());
83 let base_c = [0.60f32, 0.65, 0.74];
84 let n_seg = 24usize;
85 let mut prev = (x_l, y_of(self.value_at(0.0)));
86 for i in 1..=n_seg {
87 let t = i as f32 / n_seg as f32;
88 let x = x_l + (x_r - x_l) * t;
89 let y = y_of(self.value_at(t));
90 let (dx, dy) = (x - prev.0, y - prev.1);
91 let len = (dx * dx + dy * dy).sqrt().max(1e-3);
92 let (nx, ny) = (dy / len, -dx / len);
93 let lit = (nx * lx + ny * ly) * 0.35;
94 let col = [
95 (base_c[0] + lit).clamp(0.0, 1.0),
96 (base_c[1] + lit).clamp(0.0, 1.0),
97 (base_c[2] + lit).clamp(0.0, 1.0),
98 1.0,
99 ];
100 ctx.vector(prev.0, prev.1, x, y, 1.5, col, Cap::Round);
101 prev = (x, y);
102 }
103
104 ctx.well_rim(rect, radius, crate::layout::control_relief());
105 }
106 }
107
108 impl Input for RampPreview {
109 fn on_event(&mut self, event: &Event, _ectx: &mut EventCtx) -> bool {
110 match event {
111 Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
112 self.just_clicked = true;
113 true
114 }
115 Event::MouseEnter => {
116 self.hovered = true;
117 false
118 }
119 Event::MouseLeave => {
120 self.hovered = false;
121 false
122 }
123 _ => false,
124 }
125 }
126
127 fn take_click(&mut self) -> bool {
128 std::mem::take(&mut self.just_clicked)
129 }
130 }
131
132 #[cfg(test)]
133 mod tests {
134 use super::*;
135
136 #[test]
137 fn spec_parse_and_interpolate() {
138 let mut p = RampPreview {
139 keys: vec![],
140 smooth: false,
141 just_clicked: false,
142 hovered: false,
143 };
144 p.set_spec_str("linear;0.0:0.0,0.5:1.0,1.0:0.0");
145 assert!((p.value_at(0.25) - 0.5).abs() < 1e-4);
146 assert!((p.value_at(0.5) - 1.0).abs() < 1e-4);
147 // Unparsable falls back to the identity, not to empty.
148 p.set_spec_str("garbage");
149 assert!((p.value_at(0.5) - 0.5).abs() < 1e-4);
150 }
151 }