egui_elements/widgets/particles/lightning/
settings.rs1use egui::{Color32, Context, DragValue, Slider, Ui};
4
5use super::super::shared::{ParticleLayer, begin_body, settings_window};
6use super::Lightning;
7use super::effect::{
8 DEFAULT_AMBIENT_INTERVAL_SECS, DEFAULT_BOLT_COUNT, DEFAULT_BRANCH_DEPTH, DEFAULT_COLOR,
9 DEFAULT_DURATION_MS, DEFAULT_FLASH_INTENSITY, DEFAULT_JITTER, DEFAULT_THICKNESS,
10};
11
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[derive(Debug, Clone, PartialEq)]
19pub struct LightningSettings {
20 pub open: bool,
22 pub active: bool,
24 pub color: Color32,
26 pub bolt_count: usize,
28 pub branch_depth: u32,
30 pub thickness: f32,
32 pub jitter: f32,
34 pub flash_intensity: f32,
36 pub duration_ms: f32,
38 pub ambient_interval_secs: f32,
40 pub layer: ParticleLayer,
42}
43
44impl Default for LightningSettings {
45 fn default() -> Self {
46 Self {
47 open: false,
48 active: true,
49 color: DEFAULT_COLOR,
50 bolt_count: DEFAULT_BOLT_COUNT,
51 branch_depth: DEFAULT_BRANCH_DEPTH,
52 thickness: DEFAULT_THICKNESS,
53 jitter: DEFAULT_JITTER,
54 flash_intensity: DEFAULT_FLASH_INTENSITY,
55 duration_ms: DEFAULT_DURATION_MS,
56 ambient_interval_secs: DEFAULT_AMBIENT_INTERVAL_SECS,
57 layer: ParticleLayer::Foreground,
58 }
59 }
60}
61
62impl LightningSettings {
63 pub const STRIKE_ID: &'static str = "lightning";
69
70 pub fn reset(&mut self) {
72 let open = self.open;
73 *self = Self::default();
74 self.open = open;
75 }
76
77 pub fn show_window(&mut self, ctx: &Context) -> Option<egui::Response> {
82 let mut open = self.open;
84 let resp = settings_window(ctx, "Lightning Settings", &mut open, |ui| {
85 self.body_ui(ui)
86 });
87 self.open = open;
88 resp
89 }
90
91 pub fn body_ui(&mut self, ui: &mut Ui) {
97 let theme = begin_body(ui);
98
99 ui.heading("General");
100 ui.checkbox(&mut self.active, "Effect Active");
101 ui.horizontal(|ui| {
102 ui.label("Color:");
103 ui.color_edit_button_srgba(&mut self.color);
104 });
105
106 ui.separator();
107 ui.heading("Bolt");
108
109 ui.horizontal(|ui| {
110 ui.label("Bolts:");
111 ui.add(Slider::new(&mut self.bolt_count, 1..=8));
112 });
113
114 ui.horizontal(|ui| {
115 ui.label("Branch Depth:");
116 ui.add(Slider::new(&mut self.branch_depth, 0..=8))
117 .on_hover_text("0 draws the trunk only; each step forks a shorter side branch");
118 });
119
120 ui.horizontal(|ui| {
121 ui.label("Thickness (px):");
122 ui.add(Slider::new(&mut self.thickness, 0.5..=12.0));
123 });
124
125 ui.horizontal(|ui| {
126 ui.label("Jitter:");
127 ui.add(Slider::new(&mut self.jitter, 0.0..=0.6))
128 .on_hover_text("Midpoint displacement as a fraction of segment length");
129 });
130
131 ui.separator();
132 ui.heading("Flash");
133
134 ui.horizontal(|ui| {
135 ui.label("Intensity:");
136 ui.add(Slider::new(&mut self.flash_intensity, 0.0..=1.0));
137 });
138
139 ui.horizontal(|ui| {
140 ui.label("Duration (ms):");
141 ui.add(Slider::new(&mut self.duration_ms, 80.0..=2000.0));
142 });
143
144 if ui.button("Strike now").clicked() {
145 Lightning::from_settings(Self::STRIKE_ID, self).strike(ui.ctx());
146 }
147
148 ui.separator();
149 ui.heading("Ambient");
150
151 ui.horizontal(|ui| {
152 ui.label("Interval (s):");
153 ui.add(
154 DragValue::new(&mut self.ambient_interval_secs)
155 .speed(0.1)
156 .range(0.0..=120.0)
157 .suffix(" s"),
158 )
159 .on_hover_text("0 disables the ambient scheduler");
160 });
161
162 ui.separator();
163 ui.heading("Rendering");
164 ui.horizontal(|ui| {
165 ui.label("Layer:");
166 ui.selectable_value(
167 &mut self.layer,
168 ParticleLayer::Background,
169 ParticleLayer::Background.to_str(),
170 )
171 .on_hover_text("Behind everything (Order::Background)");
172 ui.selectable_value(
173 &mut self.layer,
174 ParticleLayer::Foreground,
175 ParticleLayer::Foreground.to_str(),
176 )
177 .on_hover_text("In front of windows (Order::Foreground)");
178 ui.selectable_value(
179 &mut self.layer,
180 ParticleLayer::Top,
181 ParticleLayer::Top.to_str(),
182 )
183 .on_hover_text("On top of everything including debug/tooltips (Order::Debug)");
184 });
185
186 ui.add_space(theme.spacing.sm);
187 if ui.button("Reset Defaults").clicked() {
188 self.reset();
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use egui::__run_test_ctx;
197 use egui::Vec2;
198
199 #[test]
200 fn defaults_are_sane() {
201 let settings = LightningSettings::default();
202 assert!(!settings.open);
203 assert!(settings.active);
204 assert_eq!(settings.bolt_count, 1);
205 assert!(settings.branch_depth >= 1);
206 assert!(settings.thickness > 0.0);
207 assert!((0.0..=1.0).contains(&settings.jitter));
208 assert!((0.0..=1.0).contains(&settings.flash_intensity));
209 assert!(settings.duration_ms > 100.0);
210 assert_eq!(settings.ambient_interval_secs, 0.0);
211 assert_eq!(settings.layer, ParticleLayer::Foreground);
212 }
213
214 #[test]
215 fn reset_keeps_open() {
216 let mut settings = LightningSettings {
217 open: true,
218 active: false,
219 color: Color32::from_rgb(9, 9, 9),
220 bolt_count: 7,
221 branch_depth: 5,
222 thickness: 9.0,
223 jitter: 0.6,
224 flash_intensity: 0.1,
225 duration_ms: 900.0,
226 ambient_interval_secs: 12.0,
227 layer: ParticleLayer::Top,
228 };
229
230 settings.reset();
231
232 assert!(settings.open, "reset must not close the window");
233 assert_eq!(
234 settings,
235 LightningSettings {
236 open: true,
237 ..Default::default()
238 }
239 );
240 }
241
242 fn window_height_on(screen: Vec2) -> f32 {
248 let ctx = Context::default();
249 let mut theme = crate::theme::Theme::new(crate::theme::ThemeKind::TokyoNight);
252 theme.install(&ctx);
253 let mut settings = LightningSettings {
254 open: true,
255 ..Default::default()
256 };
257 let mut size = Vec2::ZERO;
258 for _ in 0..2 {
259 let output = ctx.run_ui(
260 egui::RawInput {
261 screen_rect: Some(egui::Rect::from_min_size(
262 egui::Pos2::ZERO,
263 screen,
264 )),
265 ..Default::default()
266 },
267 |ui| {
268 if let Some(resp) = settings.show_window(ui.ctx()) {
269 size = resp.rect.size();
270 }
271 },
272 );
273 output.drop_without_applying_deltas();
274 }
275 size.y
276 }
277
278 #[test]
279 fn settings_body_fits_the_demo_root() {
280 let height = window_height_on(Vec2::new(900.0, 2000.0));
281 assert!(
282 height <= 650.0,
283 "settings window is {height} tall, root is 650"
284 );
285 }
286
287 #[test]
288 fn body_ui_renders_without_a_window() {
289 let mut settings = LightningSettings::default();
290 assert!(!settings.open);
291 egui::__run_test_ui(|ui| {
292 settings.body_ui(ui);
293 });
294 assert!(
295 !settings.open,
296 "body_ui must not touch the window flag"
297 );
298 }
299
300 #[test]
301 fn show_window_returns_response_only_when_open() {
302 __run_test_ctx(|ctx| {
303 let mut closed = LightningSettings::default();
304 assert!(closed.show_window(ctx).is_none());
305 });
306
307 __run_test_ctx(|ctx| {
308 let mut open = LightningSettings {
309 open: true,
310 ..Default::default()
311 };
312 assert!(open.show_window(ctx).is_some());
313 });
314 }
315}