git.lucas.co / cce-core
GUI-free half of the cce toolkit: config, input, IPC, spec parsers
git clone https://git.lucas.co/cce-core.git

src/droplet.rs (9K)

  1 //! The droplet spec: the shape and material knobs of the DE's droplet — the status bar's
  2 //! module drops and the compositor's scenefx droplet node read the same string, so the two
  3 //! sides can never disagree on a field. Moved here from `cce_ui::scene::paint`, which
  4 //! re-exports it, so the compositor can parse a spec without linking the toolkit.
  5 
  6 /// Shape and material knobs for `cce_ui::scene::paint::Prim::Droplet`. Fractions are of the
  7 /// droplet rect's height unless said otherwise, so a spec is resolution- and
  8 /// module-size-independent; the tessellator resolves and clamps them against
  9 /// the concrete rect.
 10 #[derive(Clone, Copy, Debug, PartialEq)]
 11 pub struct DropletSpec {
 12     /// How far the sheet's bottom lifts above the rect bottom (the waist the
 13     /// sides pull up into), fraction of height. 0 = no waist (a capsule).
 14     pub sag: f32,
 15     /// Belly capsule radius, fraction of height. **≤ 0 disables the belly**:
 16     /// the drop is the sheet alone — with `attach` and `sheet_r` rounding its
 17     /// top and bottom this is the oval dewdrop, and the default.
 18     pub belly: f32,
 19     /// Belly half-width, fraction of the half-width left after the belly
 20     /// radius (1 = the belly spans the whole bottom).
 21     pub belly_w: f32,
 22     /// Smooth-union blend distance, fraction of height — bigger = softer neck
 23     /// between sheet and belly.
 24     pub blend: f32,
 25     /// Sheet bottom-corner radius, fraction of height.
 26     pub sheet_r: f32,
 27     /// Sheet TOP-corner radius (the meniscus taper at the attach line),
 28     /// fraction of height. 0 = the sides meet the attach edge square (the
 29     /// clinging-pool look); larger values narrow the contact span so the
 30     /// silhouette curves into the edge like a dewdrop. When `attach + sheet_r`
 31     /// exceeds the sheet height the pair scales down proportionally, so 0.5 +
 32     /// 0.5 is the fully continuous egg curve with no straight side segment.
 33     pub attach: f32,
 34     /// Tint opacity at the deep interior relative to the color's own alpha;
 35     /// the rim falls toward `clarity` × that (thin water is clearer). 1 = flat.
 36     pub clarity: f32,
 37     /// Dome slope amplitude: scales the surface tilt the shading sees.
 38     pub dome: f32,
 39     /// Shaded band width (the dome's curved skirt), fraction of height.
 40     pub band: f32,
 41     /// Specular (gleam) strength — replaces the DE material's slot.
 42     pub gleam: f32,
 43     /// Wet-surface shininess exponent.
 44     pub shine: f32,
 45     /// Fresnel rim crest amplitude (the glass-edge brightening).
 46     pub rim: f32,
 47     /// Bottom bow: the drop's bottom boundary becomes ONE continuous circular
 48     /// arc — lowest at center, rising by `bow` (fraction of height) at the
 49     /// drop's side extents. The arc's radius is derived per drop from that
 50     /// fixed edge rise, so a wide drop gets a huge radius and the curvature
 51     /// stays subtle at the middle while a narrow drop curves visibly. 0
 52     /// disables it (flat bottom run between the corner arcs).
 53     pub bow: f32,
 54     /// Corner-curve exponent for the silhouette (and the dome profile riding
 55     /// it): 2 = circular arcs, above 2 = superellipse quadrants whose
 56     /// curvature ramps to ZERO at both ends of each arc — every junction
 57     /// (attach↔side, side↔bottom, curve↔flat top) becomes curvature-
 58     /// continuous, so unequal attach/sheet_r radii read as ONE flowing curve
 59     /// instead of two arcs meeting, and the contact eases out of the flat
 60     /// top like a meniscus. Clamped to [2, 6].
 61     pub curve: f32,
 62     /// Extra tint density at the drop's deep interior: the body opacity ramps
 63     /// from `clarity` at the rim up to `1 + core` (× the color's own alpha,
 64     /// clamped to opaque) inside — the water reads thickest in the middle,
 65     /// which is also where a module's text sits, so glyphs get a calmer
 66     /// field without giving up the watery rim. 0 = the original flat
 67     /// interior falloff.
 68     pub core: f32,
 69     /// Refraction strength in logical px — how far the COMPOSITOR's droplet
 70     /// backdrop pass bends the image behind the drop at the rim. Client-side
 71     /// rendering ignores it (a Wayland client cannot see behind its own
 72     /// window); the compositor reads the same spec and drives its scenefx
 73     /// droplet node with it. 0 disables the backdrop pass.
 74     pub refr: f32,
 75     /// Strength (0-1) of the compositor pass's inverted lens ghost — the
 76     /// faint upside-down image of the scene a real hanging drop shows in its
 77     /// belly. Client-side ignored, like `refr`.
 78     pub ghost: f32,
 79     /// Contact-shadow strength (0-1): a soft dark falloff cast below the
 80     /// drop's lower arc, outside the silhouette — the volume cue of a bead
 81     /// sitting proud of the surface. The host must leave room beneath the
 82     /// drop box for it (the status bar insets the box by
 83     /// [`DropletSpec::shadow_gap`]). 0 disables it.
 84     pub shadow: f32,
 85 }
 86 
 87 impl DropletSpec {
 88     /// Parse the DE's droplet spec string — whitespace-separated `k=v` pairs
 89     /// onto the defaults (an empty string is all defaults). Unknown keys and
 90     /// non-numeric values `log::warn!` and are skipped, so a typo surfaces in
 91     /// the log instead of silently reverting one knob. Shared by the status
 92     /// bar (which draws the drop) and the compositor (whose scenefx droplet
 93     /// node refracts the backdrop behind it) so the two sides can never
 94     /// disagree about a spec's meaning.
 95     pub fn parse(raw: &str) -> Self {
 96         let mut spec = Self::default();
 97         for tok in raw.split_whitespace() {
 98             let Some((key, val)) = tok.split_once('=') else {
 99                 log::warn!("droplet spec: token '{}' is not k=v — skipped", tok);
100                 continue;
101             };
102             let Ok(v) = val.parse::<f32>() else {
103                 log::warn!("droplet spec: '{}' has a non-numeric value — skipped", tok);
104                 continue;
105             };
106             match key {
107                 "sag" => spec.sag = v,
108                 "belly" => spec.belly = v,
109                 "belly_w" => spec.belly_w = v,
110                 "blend" => spec.blend = v,
111                 "sheet_r" => spec.sheet_r = v,
112                 "attach" => spec.attach = v,
113                 "clarity" => spec.clarity = v,
114                 "dome" => spec.dome = v,
115                 "band" => spec.band = v,
116                 "gleam" => spec.gleam = v,
117                 "shine" => spec.shine = v,
118                 "rim" => spec.rim = v,
119                 "bow" => spec.bow = v,
120                 "curve" => spec.curve = v,
121                 "core" => spec.core = v,
122                 "refr" => spec.refr = v,
123                 "ghost" => spec.ghost = v,
124                 "shadow" => spec.shadow = v,
125                 _ => log::warn!("droplet spec: unknown key '{}' — skipped", key),
126             }
127         }
128         spec
129     }
130 
131     /// Resolve the silhouette's height-fraction knobs against a concrete rect
132     /// (logical px) with the SAME clamps the tessellator applies: returns
133     /// `(sheet_r, attach_r, bow_rise)` in logical px, the attach/sheet pair
134     /// proportionally scaled down when it overfills the height. The
135     /// compositor's droplet backdrop node uses this so its refracting
136     /// silhouette and the client-drawn drop are the same shape.
137     pub fn resolve_silhouette(&self, w: f32, h: f32) -> (f32, f32, f32) {
138         let hx = w * 0.5;
139         let hy = h * 0.5;
140         let mut sr = (self.sheet_r.clamp(0.0, 1.0) * h).min(hx);
141         let mut ar = (self.attach.clamp(0.0, 1.0) * h).min(hx);
142         let sheet_h = 2.0 * hy;
143         if sr + ar > sheet_h && sr + ar > 0.0 {
144             let f = sheet_h / (sr + ar);
145             sr *= f;
146             ar *= f;
147         }
148         let bow = (self.bow.clamp(0.0, 0.5) * h).min(hy * 0.9);
149         (sr, ar, bow)
150     }
151 
152     /// Vertical room (logical px) a host should leave BELOW the drop box for
153     /// the contact shadow, given the full slot height. One place, so the
154     /// bar's reserved gap and the shader's falloff reach stay proportioned.
155     pub fn shadow_gap(&self, slot_h: f32) -> f32 {
156         if self.shadow > 0.0 {
157             (0.16 * slot_h).ceil()
158         } else {
159             0.0
160         }
161     }
162 }
163 
164 impl Default for DropletSpec {
165     fn default() -> Self {
166         // The oval dewdrop: no belly, no sag — one continuous curve from a
167         // tapered attach line to a fully round bottom. attach + sheet_r fill
168         // the whole height (no straight side segment), biased bottom-heavy,
169         // and the superellipse curve exponent keeps the unequal pair
170         // curvature-continuous. The pendant-pool look is reachable by
171         // setting `belly` > 0 (and usually some `sag`).
172         Self {
173             sag: 0.0,
174             belly: 0.0,
175             belly_w: 0.5,
176             blend: 0.35,
177             sheet_r: 0.58,
178             attach: 0.42,
179             clarity: 0.5,
180             dome: 0.9,
181             band: 0.9,
182             gleam: 1.4,
183             shine: 32.0,
184             rim: 0.5,
185             bow: 0.12,
186             curve: 2.6,
187             core: 0.35,
188             refr: 0.0,
189             ghost: 0.0,
190             shadow: 0.35,
191         }
192     }
193 }