1use bevy::{color::palettes::css::*, prelude::*, time::Time, window::RequestRedraw};
4
5const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
6const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
7const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
8
9const SHAPE_DEFAULT_SETTINGS: ShapeSettings = ShapeSettings { index: 0 };
10
11const SHADOW_DEFAULT_SETTINGS: ShadowSettings = ShadowSettings {
12 x_offset: 20.0,
13 y_offset: 20.0,
14 blur: 10.0,
15 spread: 15.0,
16 count: 1,
17 samples: 6,
18};
19
20const SHAPES: &[(&str, fn(&mut Node, &mut BorderRadius))] = &[
21 ("1", |node, radius| {
22 node.width = px(164);
23 node.height = px(164);
24 *radius = BorderRadius::ZERO;
25 }),
26 ("2", |node, radius| {
27 node.width = px(164);
28 node.height = px(164);
29 *radius = BorderRadius::all(px(41));
30 }),
31 ("3", |node, radius| {
32 node.width = px(164);
33 node.height = px(164);
34 *radius = BorderRadius::MAX;
35 }),
36 ("4", |node, radius| {
37 node.width = px(240);
38 node.height = px(80);
39 *radius = BorderRadius::all(px(32));
40 }),
41 ("5", |node, radius| {
42 node.width = px(80);
43 node.height = px(240);
44 *radius = BorderRadius::all(px(32));
45 }),
46];
47
48#[derive(Resource, Default)]
49struct ShapeSettings {
50 index: usize,
51}
52
53#[derive(Resource, Default)]
54struct ShadowSettings {
55 x_offset: f32,
56 y_offset: f32,
57 blur: f32,
58 spread: f32,
59 count: usize,
60 samples: u32,
61}
62
63#[derive(Component)]
64struct ShadowNode;
65
66#[derive(Component, PartialEq, Clone, Copy)]
67enum SettingsButton {
68 XOffsetInc,
69 XOffsetDec,
70 YOffsetInc,
71 YOffsetDec,
72 BlurInc,
73 BlurDec,
74 SpreadInc,
75 SpreadDec,
76 CountInc,
77 CountDec,
78 ShapePrev,
79 ShapeNext,
80 Reset,
81 SamplesInc,
82 SamplesDec,
83}
84
85#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
86enum SettingType {
87 XOffset,
88 YOffset,
89 Blur,
90 Spread,
91 Count,
92 Shape,
93 Samples,
94}
95
96impl SettingType {
97 fn label(&self) -> &str {
98 match self {
99 SettingType::XOffset => "X Offset",
100 SettingType::YOffset => "Y Offset",
101 SettingType::Blur => "Blur",
102 SettingType::Spread => "Spread",
103 SettingType::Count => "Count",
104 SettingType::Shape => "Shape",
105 SettingType::Samples => "Samples",
106 }
107 }
108}
109
110#[derive(Resource, Default)]
111struct HeldButton {
112 button: Option<SettingsButton>,
113 pressed_at: Option<f64>,
114 last_repeat: Option<f64>,
115}
116
117fn main() {
118 App::new()
119 .add_plugins(DefaultPlugins)
120 .insert_resource(SHADOW_DEFAULT_SETTINGS)
121 .insert_resource(SHAPE_DEFAULT_SETTINGS)
122 .insert_resource(HeldButton::default())
123 .add_systems(Startup, setup)
124 .add_systems(
125 Update,
126 (
127 button_system,
128 button_color_system,
129 update_shape.run_if(resource_changed::<ShapeSettings>),
130 update_shadow.run_if(resource_changed::<ShadowSettings>),
131 update_shadow_samples.run_if(resource_changed::<ShadowSettings>),
132 button_repeat_system,
133 ),
134 )
135 .run();
136}
137
138fn setup(
140 mut commands: Commands,
141 asset_server: Res<AssetServer>,
142 shadow: Res<ShadowSettings>,
143 shape: Res<ShapeSettings>,
144) {
145 commands.spawn((Camera2d, BoxShadowSamples(shadow.samples)));
146 commands
148 .spawn((
149 Node {
150 width: percent(100),
151 height: percent(100),
152 align_items: AlignItems::Center,
153 justify_content: JustifyContent::Center,
154 ..default()
155 },
156 BackgroundColor(GRAY.into()),
157 ))
158 .insert(children![{
159 let mut node = Node {
160 width: px(164),
161 height: px(164),
162 border: UiRect::all(px(1)),
163 align_items: AlignItems::Center,
164 justify_content: JustifyContent::Center,
165 ..default()
166 };
167 let mut radius = BorderRadius::ZERO;
168 SHAPES[shape.index % SHAPES.len()].1(&mut node, &mut radius);
169
170 (
171 node,
172 BorderColor::all(WHITE),
173 radius,
174 BackgroundColor(Color::srgb(0.21, 0.21, 0.21)),
175 BoxShadow(vec![ShadowStyle {
176 color: Color::BLACK.with_alpha(0.8),
177 x_offset: px(shadow.x_offset),
178 y_offset: px(shadow.y_offset),
179 spread_radius: px(shadow.spread),
180 blur_radius: px(shadow.blur),
181 }]),
182 ShadowNode,
183 )
184 }]);
185
186 commands
188 .spawn((
189 Node {
190 flex_direction: FlexDirection::Column,
191 position_type: PositionType::Absolute,
192 left: px(24),
193 bottom: px(24),
194 width: px(270),
195 padding: UiRect::all(px(16)),
196 ..default()
197 },
198 BackgroundColor(Color::srgb(0.12, 0.12, 0.12).with_alpha(0.85)),
199 BorderColor::all(Color::WHITE.with_alpha(0.15)),
200 BorderRadius::all(px(12)),
201 ZIndex(10),
202 ))
203 .insert(children![
204 build_setting_row(
205 SettingType::Shape,
206 SettingsButton::ShapePrev,
207 SettingsButton::ShapeNext,
208 shape.index as f32,
209 &asset_server,
210 ),
211 build_setting_row(
212 SettingType::XOffset,
213 SettingsButton::XOffsetDec,
214 SettingsButton::XOffsetInc,
215 shadow.x_offset,
216 &asset_server,
217 ),
218 build_setting_row(
219 SettingType::YOffset,
220 SettingsButton::YOffsetDec,
221 SettingsButton::YOffsetInc,
222 shadow.y_offset,
223 &asset_server,
224 ),
225 build_setting_row(
226 SettingType::Blur,
227 SettingsButton::BlurDec,
228 SettingsButton::BlurInc,
229 shadow.blur,
230 &asset_server,
231 ),
232 build_setting_row(
233 SettingType::Spread,
234 SettingsButton::SpreadDec,
235 SettingsButton::SpreadInc,
236 shadow.spread,
237 &asset_server,
238 ),
239 build_setting_row(
240 SettingType::Count,
241 SettingsButton::CountDec,
242 SettingsButton::CountInc,
243 shadow.count as f32,
244 &asset_server,
245 ),
246 build_setting_row(
248 SettingType::Samples,
249 SettingsButton::SamplesDec,
250 SettingsButton::SamplesInc,
251 shadow.samples as f32,
252 &asset_server,
253 ),
254 (
256 Node {
257 flex_direction: FlexDirection::Row,
258 align_items: AlignItems::Center,
259 height: px(36),
260 margin: UiRect::top(px(12)),
261 ..default()
262 },
263 children![(
264 Button,
265 Node {
266 width: px(90),
267 height: px(32),
268 justify_content: JustifyContent::Center,
269 align_items: AlignItems::Center,
270 ..default()
271 },
272 BackgroundColor(NORMAL_BUTTON),
273 BorderRadius::all(px(8)),
274 SettingsButton::Reset,
275 children![(
276 Text::new("Reset"),
277 TextFont {
278 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
279 font_size: 16.0,
280 ..default()
281 },
282 )],
283 )],
284 ),
285 ]);
286}
287
288fn build_setting_row(
292 setting_type: SettingType,
293 dec: SettingsButton,
294 inc: SettingsButton,
295 value: f32,
296 asset_server: &Res<AssetServer>,
297) -> impl Bundle {
298 let value_text = match setting_type {
299 SettingType::Shape => SHAPES[value as usize % SHAPES.len()].0.to_string(),
300 SettingType::Count => format!("{}", value as usize),
301 _ => format!("{value:.1}"),
302 };
303
304 (
305 Node {
306 flex_direction: FlexDirection::Row,
307 align_items: AlignItems::Center,
308 height: px(32),
309 ..default()
310 },
311 children![
312 (
313 Node {
314 width: px(80),
315 justify_content: JustifyContent::FlexEnd,
316 align_items: AlignItems::Center,
317 ..default()
318 },
319 children![(
321 Text::new(setting_type.label()),
322 TextFont {
323 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
324 font_size: 16.0,
325 ..default()
326 },
327 )],
328 ),
329 (
330 Button,
331 Node {
332 width: px(28),
333 height: px(28),
334 margin: UiRect::left(px(8)),
335 justify_content: JustifyContent::Center,
336 align_items: AlignItems::Center,
337 ..default()
338 },
339 BackgroundColor(Color::WHITE),
340 BorderRadius::all(px(6)),
341 dec,
342 children![(
343 Text::new(if setting_type == SettingType::Shape {
344 "<"
345 } else {
346 "-"
347 }),
348 TextFont {
349 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
350 font_size: 18.0,
351 ..default()
352 },
353 )],
354 ),
355 (
356 Node {
357 width: px(48),
358 height: px(28),
359 margin: UiRect::horizontal(px(8)),
360 justify_content: JustifyContent::Center,
361 align_items: AlignItems::Center,
362 ..default()
363 },
364 BorderRadius::all(px(6)),
365 children![{
366 (
367 Text::new(value_text),
368 TextFont {
369 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
370 font_size: 16.0,
371 ..default()
372 },
373 setting_type,
374 )
375 }],
376 ),
377 (
378 Button,
379 Node {
380 width: px(28),
381 height: px(28),
382 justify_content: JustifyContent::Center,
383 align_items: AlignItems::Center,
384 ..default()
385 },
386 BackgroundColor(Color::WHITE),
387 BorderRadius::all(px(6)),
388 inc,
389 children![(
390 Text::new(if setting_type == SettingType::Shape {
391 ">"
392 } else {
393 "+"
394 }),
395 TextFont {
396 font: asset_server.load("fonts/FiraSans-Bold.ttf"),
397 font_size: 18.0,
398 ..default()
399 },
400 )],
401 ),
402 ],
403 )
404}
405
406fn update_shadow(
410 shadow: Res<ShadowSettings>,
411 mut query: Query<&mut BoxShadow, With<ShadowNode>>,
412 mut label_query: Query<(&mut Text, &SettingType)>,
413) {
414 for mut box_shadow in &mut query {
415 *box_shadow = BoxShadow(generate_shadows(&shadow));
416 }
417 for (mut text, setting) in &mut label_query {
419 let value = match setting {
420 SettingType::XOffset => format!("{:.1}", shadow.x_offset),
421 SettingType::YOffset => format!("{:.1}", shadow.y_offset),
422 SettingType::Blur => format!("{:.1}", shadow.blur),
423 SettingType::Spread => format!("{:.1}", shadow.spread),
424 SettingType::Count => format!("{}", shadow.count),
425 SettingType::Shape => continue,
426 SettingType::Samples => format!("{}", shadow.samples),
427 };
428 *text = Text::new(value);
429 }
430}
431
432fn update_shadow_samples(
433 shadow: Res<ShadowSettings>,
434 mut query: Query<&mut BoxShadowSamples, With<Camera2d>>,
435) {
436 for mut samples in &mut query {
437 samples.0 = shadow.samples;
438 }
439}
440
441fn generate_shadows(shadow: &ShadowSettings) -> Vec<ShadowStyle> {
442 match shadow.count {
443 1 => vec![make_shadow(
444 BLACK.into(),
445 shadow.x_offset,
446 shadow.y_offset,
447 shadow.spread,
448 shadow.blur,
449 )],
450 2 => vec![
451 make_shadow(
452 BLUE.into(),
453 shadow.x_offset,
454 shadow.y_offset,
455 shadow.spread,
456 shadow.blur,
457 ),
458 make_shadow(
459 YELLOW.into(),
460 -shadow.x_offset,
461 -shadow.y_offset,
462 shadow.spread,
463 shadow.blur,
464 ),
465 ],
466 3 => vec![
467 make_shadow(
468 BLUE.into(),
469 shadow.x_offset,
470 shadow.y_offset,
471 shadow.spread,
472 shadow.blur,
473 ),
474 make_shadow(
475 YELLOW.into(),
476 -shadow.x_offset,
477 -shadow.y_offset,
478 shadow.spread,
479 shadow.blur,
480 ),
481 make_shadow(
482 RED.into(),
483 shadow.y_offset,
484 -shadow.x_offset,
485 shadow.spread,
486 shadow.blur,
487 ),
488 ],
489 _ => vec![],
490 }
491}
492
493fn make_shadow(color: Color, x_offset: f32, y_offset: f32, spread: f32, blur: f32) -> ShadowStyle {
494 ShadowStyle {
495 color: color.with_alpha(0.8),
496 x_offset: px(x_offset),
497 y_offset: px(y_offset),
498 spread_radius: px(spread),
499 blur_radius: px(blur),
500 }
501}
502
503fn update_shape(
505 shape: Res<ShapeSettings>,
506 mut query: Query<(&mut Node, &mut BorderRadius), With<ShadowNode>>,
507 mut label_query: Query<(&mut Text, &SettingType)>,
508) {
509 for (mut node, mut radius) in &mut query {
510 SHAPES[shape.index % SHAPES.len()].1(&mut node, &mut radius);
511 }
512 for (mut text, kind) in &mut label_query {
513 if *kind == SettingType::Shape {
514 *text = Text::new(SHAPES[shape.index % SHAPES.len()].0);
515 }
516 }
517}
518
519fn button_system(
521 mut interaction_query: Query<
522 (&Interaction, &SettingsButton),
523 (Changed<Interaction>, With<Button>),
524 >,
525 mut shadow: ResMut<ShadowSettings>,
526 mut shape: ResMut<ShapeSettings>,
527 mut held: ResMut<HeldButton>,
528 time: Res<Time>,
529) {
530 let now = time.elapsed_secs_f64();
531 for (interaction, btn) in &mut interaction_query {
532 match *interaction {
533 Interaction::Pressed => {
534 trigger_button_action(btn, &mut shadow, &mut shape);
535 held.button = Some(*btn);
536 held.pressed_at = Some(now);
537 held.last_repeat = Some(now);
538 }
539 Interaction::None | Interaction::Hovered => {
540 if held.button == Some(*btn) {
541 held.button = None;
542 held.pressed_at = None;
543 held.last_repeat = None;
544 }
545 }
546 }
547 }
548}
549
550fn trigger_button_action(
551 btn: &SettingsButton,
552 shadow: &mut ShadowSettings,
553 shape: &mut ShapeSettings,
554) {
555 match btn {
556 SettingsButton::XOffsetInc => shadow.x_offset += 1.0,
557 SettingsButton::XOffsetDec => shadow.x_offset -= 1.0,
558 SettingsButton::YOffsetInc => shadow.y_offset += 1.0,
559 SettingsButton::YOffsetDec => shadow.y_offset -= 1.0,
560 SettingsButton::BlurInc => shadow.blur = (shadow.blur + 1.0).max(0.0),
561 SettingsButton::BlurDec => shadow.blur = (shadow.blur - 1.0).max(0.0),
562 SettingsButton::SpreadInc => shadow.spread += 1.0,
563 SettingsButton::SpreadDec => shadow.spread -= 1.0,
564 SettingsButton::CountInc => {
565 if shadow.count < 3 {
566 shadow.count += 1;
567 }
568 }
569 SettingsButton::CountDec => {
570 if shadow.count > 1 {
571 shadow.count -= 1;
572 }
573 }
574 SettingsButton::ShapePrev => {
575 if shape.index == 0 {
576 shape.index = SHAPES.len() - 1;
577 } else {
578 shape.index -= 1;
579 }
580 }
581 SettingsButton::ShapeNext => {
582 shape.index = (shape.index + 1) % SHAPES.len();
583 }
584 SettingsButton::Reset => {
585 *shape = SHAPE_DEFAULT_SETTINGS;
586 *shadow = SHADOW_DEFAULT_SETTINGS;
587 }
588 SettingsButton::SamplesInc => shadow.samples += 1,
589 SettingsButton::SamplesDec => {
590 if shadow.samples > 1 {
591 shadow.samples -= 1;
592 }
593 }
594 }
595}
596
597fn button_repeat_system(
599 time: Res<Time>,
600 mut held: ResMut<HeldButton>,
601 mut shadow: ResMut<ShadowSettings>,
602 mut shape: ResMut<ShapeSettings>,
603 mut request_redraw_writer: MessageWriter<RequestRedraw>,
604) {
605 if held.button.is_some() {
606 request_redraw_writer.write(RequestRedraw);
607 }
608 const INITIAL_DELAY: f64 = 0.15;
609 const REPEAT_RATE: f64 = 0.08;
610 if let (Some(btn), Some(pressed_at)) = (held.button, held.pressed_at) {
611 let now = time.elapsed_secs_f64();
612 let since_pressed = now - pressed_at;
613 let last_repeat = held.last_repeat.unwrap_or(pressed_at);
614 let since_last = now - last_repeat;
615 if since_pressed > INITIAL_DELAY && since_last > REPEAT_RATE {
616 trigger_button_action(&btn, &mut shadow, &mut shape);
617 held.last_repeat = Some(now);
618 }
619 }
620}
621
622fn button_color_system(
624 mut query: Query<
625 (&Interaction, &mut BackgroundColor),
626 (Changed<Interaction>, With<Button>, With<SettingsButton>),
627 >,
628) {
629 for (interaction, mut color) in &mut query {
630 match *interaction {
631 Interaction::Pressed => *color = PRESSED_BUTTON.into(),
632 Interaction::Hovered => *color = HOVERED_BUTTON.into(),
633 Interaction::None => *color = NORMAL_BUTTON.into(),
634 }
635 }
636}