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 }