git.lucas.co / cce-ui
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 }