egui_elements/widgets/particles/light/
settings.rs1use egui::{Color32, Context, DragValue, Slider, Ui, Vec2};
4
5use super::super::shared::{ParticleLayer, begin_body, settings_window};
6use super::LightMotion;
7
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[derive(Debug, Clone, PartialEq)]
15pub struct LightSettings {
16 pub open: bool,
18 pub active: bool,
20 pub color: Color32,
22 pub density: usize,
24 pub radius_min: f32,
26 pub radius_max: f32,
28 pub intensity: f32,
30 pub motion: LightMotion,
32 pub wind: Vec2,
34 pub flicker: f32,
36 pub lifetime_min: f32,
38 pub lifetime_max: f32,
40 pub layer: ParticleLayer,
42}
43
44impl Default for LightSettings {
45 fn default() -> Self {
46 Self {
47 open: false,
48 active: true,
49 color: Color32::from_rgb(255, 224, 178),
50 density: 48,
51 radius_min: 8.0,
52 radius_max: 36.0,
53 intensity: 0.7,
54 motion: LightMotion::Drift,
55 wind: Vec2::ZERO,
56 flicker: 0.25,
57 lifetime_min: 4.0,
58 lifetime_max: 9.0,
59 layer: ParticleLayer::Foreground,
60 }
61 }
62}
63
64impl LightSettings {
65 pub fn reset(&mut self) {
67 let open = self.open;
68 *self = Self::default();
69 self.open = open;
70 }
71
72 pub fn show_window(&mut self, ctx: &Context) -> Option<egui::Response> {
77 let mut open = self.open;
79 let resp = settings_window(ctx, "Light Settings", &mut open, |ui| {
80 self.body_ui(ui)
81 });
82 self.open = open;
83 resp
84 }
85
86 pub fn body_ui(&mut self, ui: &mut Ui) {
92 let theme = begin_body(ui);
93
94 ui.heading("General");
95 ui.checkbox(&mut self.active, "Effect Active");
96 ui.horizontal(|ui| {
97 ui.label("Color:");
98 ui.color_edit_button_srgba(&mut self.color);
99 });
100
101 ui.separator();
102 ui.heading("Motion");
103 ui.horizontal(|ui| {
104 ui.label("Motion:");
105 ui.selectable_value(
106 &mut self.motion,
107 LightMotion::Drift,
108 LightMotion::Drift.to_str(),
109 )
110 .on_hover_text("Slow wandering motes that bounce off the area edges");
111 ui.selectable_value(
112 &mut self.motion,
113 LightMotion::Rise,
114 LightMotion::Rise.to_str(),
115 )
116 .on_hover_text("Embers that rise from the bottom edge and sway as they climb");
117 });
118
119 ui.separator();
120 ui.heading("Particles");
121
122 ui.horizontal(|ui| {
123 ui.label("Density (Count):");
124 ui.add(Slider::new(&mut self.density, 0..=2000).logarithmic(true));
125 });
126
127 ui.horizontal(|ui| {
128 ui.label("Radius (px):");
129 ui.add(
130 DragValue::new(&mut self.radius_min)
131 .speed(0.5)
132 .range(1.0..=200.0)
133 .prefix("Min: "),
134 );
135 ui.add(
136 DragValue::new(&mut self.radius_max)
137 .speed(0.5)
138 .range(1.0..=200.0)
139 .prefix("Max: "),
140 );
141 });
142 if self.radius_min > self.radius_max {
144 self.radius_min = self.radius_max;
145 }
146
147 ui.horizontal(|ui| {
148 ui.label("Intensity:");
149 ui.add(Slider::new(&mut self.intensity, 0.0..=1.0));
150 });
151
152 ui.horizontal(|ui| {
153 ui.label("Flicker:");
154 ui.add(Slider::new(&mut self.flicker, 0.0..=1.0));
155 });
156
157 ui.separator();
158 ui.heading("Physics");
159
160 ui.label("Wind Vector:");
161 ui.horizontal(|ui| {
162 ui.add(Slider::new(&mut self.wind.x, -200.0..=200.0).text("Horizontal"));
163 });
164 ui.horizontal(|ui| {
165 ui.add(Slider::new(&mut self.wind.y, -100.0..=100.0).text("Vertical"));
166 });
167
168 ui.horizontal(|ui| {
169 ui.label("Lifetime (s):");
170 ui.add(
171 DragValue::new(&mut self.lifetime_min)
172 .speed(0.1)
173 .range(0.5..=60.0)
174 .prefix("Min: "),
175 );
176 ui.add(
177 DragValue::new(&mut self.lifetime_max)
178 .speed(0.1)
179 .range(0.5..=60.0)
180 .prefix("Max: "),
181 );
182 });
183 if self.lifetime_min > self.lifetime_max {
184 self.lifetime_min = self.lifetime_max;
185 }
186
187 ui.separator();
188 ui.heading("Rendering");
189 ui.horizontal(|ui| {
190 ui.label("Layer:");
191 ui.selectable_value(
192 &mut self.layer,
193 ParticleLayer::Background,
194 ParticleLayer::Background.to_str(),
195 )
196 .on_hover_text("Behind everything (Order::Background)");
197 ui.selectable_value(
198 &mut self.layer,
199 ParticleLayer::Foreground,
200 ParticleLayer::Foreground.to_str(),
201 )
202 .on_hover_text("In front of windows (Order::Foreground)");
203 ui.selectable_value(
204 &mut self.layer,
205 ParticleLayer::Top,
206 ParticleLayer::Top.to_str(),
207 )
208 .on_hover_text("On top of everything including debug/tooltips (Order::Debug)");
209 });
210
211 ui.add_space(theme.spacing.sm);
212 if ui.button("Reset Defaults").clicked() {
213 self.reset();
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use egui::__run_test_ctx;
222
223 #[test]
224 fn defaults_are_sane() {
225 let settings = LightSettings::default();
226 assert!(!settings.open);
227 assert!(settings.active);
228 assert!(settings.density > 0);
229 assert!(settings.radius_min <= settings.radius_max);
230 assert!(settings.lifetime_min <= settings.lifetime_max);
231 assert!(
232 settings.lifetime_min > 1.0,
233 "lifetimes are multi-second"
234 );
235 assert_eq!(settings.motion, LightMotion::Drift);
236 assert_eq!(settings.layer, ParticleLayer::Foreground);
237 }
238
239 #[test]
240 fn reset_keeps_open() {
241 let mut settings = LightSettings {
242 open: true,
243 active: false,
244 density: 3,
245 radius_min: 90.0,
246 radius_max: 91.0,
247 intensity: 0.1,
248 motion: LightMotion::Rise,
249 wind: Vec2::new(7.0, 8.0),
250 flicker: 0.9,
251 lifetime_min: 1.0,
252 lifetime_max: 2.0,
253 layer: ParticleLayer::Top,
254 color: Color32::from_rgb(9, 9, 9),
255 };
256
257 settings.reset();
258
259 assert!(settings.open, "reset must not close the window");
260 assert_eq!(
261 settings,
262 LightSettings {
263 open: true,
264 ..Default::default()
265 }
266 );
267 }
268
269 fn window_height_on(screen: Vec2) -> f32 {
275 let ctx = Context::default();
276 let mut theme = crate::theme::Theme::new(crate::theme::ThemeKind::TokyoNight);
279 theme.install(&ctx);
280 let mut settings = LightSettings {
281 open: true,
282 ..Default::default()
283 };
284 let mut size = Vec2::ZERO;
285 for _ in 0..2 {
286 let output = ctx.run_ui(
287 egui::RawInput {
288 screen_rect: Some(egui::Rect::from_min_size(
289 egui::Pos2::ZERO,
290 screen,
291 )),
292 ..Default::default()
293 },
294 |ui| {
295 if let Some(resp) = settings.show_window(ui.ctx()) {
296 size = resp.rect.size();
297 }
298 },
299 );
300 output.drop_without_applying_deltas();
301 }
302 size.y
303 }
304
305 #[test]
306 fn settings_body_fits_the_demo_root() {
307 let height = window_height_on(Vec2::new(900.0, 2000.0));
308 assert!(
309 height <= 650.0,
310 "settings window is {height} tall, root is 650"
311 );
312 }
313
314 #[test]
315 fn body_ui_renders_without_a_window() {
316 let mut settings = LightSettings::default();
317 assert!(!settings.open);
318 egui::__run_test_ui(|ui| {
319 settings.body_ui(ui);
320 });
321 assert!(
322 !settings.open,
323 "body_ui must not touch the window flag"
324 );
325 }
326
327 #[test]
328 fn show_window_returns_response_only_when_open() {
329 __run_test_ctx(|ctx| {
330 let mut closed = LightSettings::default();
331 assert!(closed.show_window(ctx).is_none());
332 });
333
334 __run_test_ctx(|ctx| {
335 let mut open = LightSettings {
336 open: true,
337 ..Default::default()
338 };
339 assert!(open.show_window(ctx).is_some());
340 });
341 }
342}