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))] = &[
21 ("1", |node| {
22 node.width = px(164);
23 node.height = px(164);
24 node.border_radius = BorderRadius::ZERO;
25 }),
26 ("2", |node| {
27 node.width = px(164);
28 node.height = px(164);
29 node.border_radius = BorderRadius::all(px(41));
30 }),
31 ("3", |node| {
32 node.width = px(164);
33 node.height = px(164);
34 node.border_radius = BorderRadius::MAX;
35 }),
36 ("4", |node| {
37 node.width = px(240);
38 node.height = px(80);
39 node.border_radius = BorderRadius::all(px(32));
40 }),
41 ("5", |node| {
42 node.width = px(80);
43 node.height = px(240);
44 node.border_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 border_radius: BorderRadius::ZERO,
166 ..default()
167 };
168 SHAPES[shape.index % SHAPES.len()].1(&mut node);
169
170 (
171 node,
172 BorderColor::all(WHITE),
173 BackgroundColor(Color::srgb(0.21, 0.21, 0.21)),
174 BoxShadow(vec![ShadowStyle {
175 color: Color::BLACK.with_alpha(0.8),
176 x_offset: px(shadow.x_offset),
177 y_offset: px(shadow.y_offset),
178 spread_radius: px(shadow.spread),
179 blur_radius: px(shadow.blur),
180 }]),
181 ShadowNode,
182 )
183 }]);
184
185 commands
187 .spawn((
188 Node {
189 flex_direction: FlexDirection::Column,
190 position_type: PositionType::Absolute,
191 left: px(24),
192 bottom: px(24),
193 width: px(270),
194 padding: UiRect::all(px(16)),
195 border_radius: BorderRadius::all(px(12)),
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 ZIndex(10),
201 ))
202 .insert(children![
203 build_setting_row(
204 SettingType::Shape,
205 SettingsButton::ShapePrev,
206 SettingsButton::ShapeNext,
207 shape.index as f32,
208 &asset_server,
209 ),
210 build_setting_row(
211 SettingType::XOffset,
212 SettingsButton::XOffsetDec,
213 SettingsButton::XOffsetInc,
214 shadow.x_offset,
215 &asset_server,
216 ),
217 build_setting_row(
218 SettingType::YOffset,
219 SettingsButton::YOffsetDec,
220 SettingsButton::YOffsetInc,
221 shadow.y_offset,
222 &asset_server,
223 ),
224 build_setting_row(
225 SettingType::Blur,
226 SettingsButton::BlurDec,
227 SettingsButton::BlurInc,
228 shadow.blur,
229 &asset_server,
230 ),
231 build_setting_row(
232 SettingType::Spread,
233 SettingsButton::SpreadDec,
234 SettingsButton::SpreadInc,
235 shadow.spread,
236 &asset_server,
237 ),
238 build_setting_row(
239 SettingType::Count,
240 SettingsButton::CountDec,
241 SettingsButton::CountInc,
242 shadow.count as f32,
243 &asset_server,
244 ),
245 build_setting_row(
247 SettingType::Samples,
248 SettingsButton::SamplesDec,
249 SettingsButton::SamplesInc,
250 shadow.samples as f32,
251 &asset_server,
252 ),
253 (
255 Node {
256 flex_direction: FlexDirection::Row,
257 align_items: AlignItems::Center,
258 height: px(36),
259 margin: UiRect::top(px(12)),
260 ..default()
261 },
262 children![(
263 Button,
264 Node {
265 width: px(90),
266 height: px(32),
267 justify_content: JustifyContent::Center,
268 align_items: AlignItems::Center,
269 border_radius: BorderRadius::all(px(8)),
270 ..default()
271 },
272 BackgroundColor(NORMAL_BUTTON),
273 SettingsButton::Reset,
274 children![(
275 Text::new("Reset"),
276 TextFont {
277 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
278 font_size: FontSize::Px(16.0),
279 ..default()
280 },
281 )],
282 )],
283 ),
284 ]);
285}
286
287fn build_setting_row(
291 setting_type: SettingType,
292 dec: SettingsButton,
293 inc: SettingsButton,
294 value: f32,
295 asset_server: &Res<AssetServer>,
296) -> impl Bundle {
297 let value_text = match setting_type {
298 SettingType::Shape => SHAPES[value as usize % SHAPES.len()].0.to_string(),
299 SettingType::Count => format!("{}", value as usize),
300 _ => format!("{value:.1}"),
301 };
302
303 (
304 Node {
305 flex_direction: FlexDirection::Row,
306 align_items: AlignItems::Center,
307 height: px(32),
308 ..default()
309 },
310 children![
311 (
312 Node {
313 width: px(80),
314 justify_content: JustifyContent::FlexEnd,
315 align_items: AlignItems::Center,
316 ..default()
317 },
318 children![(
320 Text::new(setting_type.label()),
321 TextFont {
322 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
323 font_size: FontSize::Px(16.0),
324 ..default()
325 },
326 )],
327 ),
328 (
329 Button,
330 Node {
331 width: px(28),
332 height: px(28),
333 margin: UiRect::left(px(8)),
334 justify_content: JustifyContent::Center,
335 align_items: AlignItems::Center,
336 border_radius: BorderRadius::all(px(6)),
337 ..default()
338 },
339 BackgroundColor(Color::WHITE),
340 dec,
341 children![(
342 Text::new(if setting_type == SettingType::Shape {
343 "<"
344 } else {
345 "-"
346 }),
347 TextFont {
348 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
349 font_size: FontSize::Px(18.0),
350 ..default()
351 },
352 )],
353 ),
354 (
355 Node {
356 width: px(48),
357 height: px(28),
358 margin: UiRect::horizontal(px(8)),
359 justify_content: JustifyContent::Center,
360 align_items: AlignItems::Center,
361 border_radius: BorderRadius::all(px(6)),
362 ..default()
363 },
364 children![{
365 (
366 Text::new(value_text),
367 TextFont {
368 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
369 font_size: FontSize::Px(16.0),
370 ..default()
371 },
372 setting_type,
373 )
374 }],
375 ),
376 (
377 Button,
378 Node {
379 width: px(28),
380 height: px(28),
381 justify_content: JustifyContent::Center,
382 align_items: AlignItems::Center,
383 border_radius: BorderRadius::all(px(6)),
384 ..default()
385 },
386 BackgroundColor(Color::WHITE),
387 inc,
388 children![(
389 Text::new(if setting_type == SettingType::Shape {
390 ">"
391 } else {
392 "+"
393 }),
394 TextFont {
395 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
396 font_size: FontSize::Px(18.0),
397 ..default()
398 },
399 )],
400 ),
401 ],
402 )
403}
404
405fn update_shadow(
409 shadow: Res<ShadowSettings>,
410 mut query: Query<&mut BoxShadow, With<ShadowNode>>,
411 mut label_query: Query<(&mut Text, &SettingType)>,
412) {
413 for mut box_shadow in &mut query {
414 *box_shadow = BoxShadow(generate_shadows(&shadow));
415 }
416 for (mut text, setting) in &mut label_query {
418 let value = match setting {
419 SettingType::XOffset => format!("{:.1}", shadow.x_offset),
420 SettingType::YOffset => format!("{:.1}", shadow.y_offset),
421 SettingType::Blur => format!("{:.1}", shadow.blur),
422 SettingType::Spread => format!("{:.1}", shadow.spread),
423 SettingType::Count => format!("{}", shadow.count),
424 SettingType::Shape => continue,
425 SettingType::Samples => format!("{}", shadow.samples),
426 };
427 *text = Text::new(value);
428 }
429}
430
431fn update_shadow_samples(
432 shadow: Res<ShadowSettings>,
433 mut query: Query<&mut BoxShadowSamples, With<Camera2d>>,
434) {
435 for mut samples in &mut query {
436 samples.0 = shadow.samples;
437 }
438}
439
440fn generate_shadows(shadow: &ShadowSettings) -> Vec<ShadowStyle> {
441 match shadow.count {
442 1 => vec![make_shadow(
443 BLACK.into(),
444 shadow.x_offset,
445 shadow.y_offset,
446 shadow.spread,
447 shadow.blur,
448 )],
449 2 => vec![
450 make_shadow(
451 BLUE.into(),
452 shadow.x_offset,
453 shadow.y_offset,
454 shadow.spread,
455 shadow.blur,
456 ),
457 make_shadow(
458 YELLOW.into(),
459 -shadow.x_offset,
460 -shadow.y_offset,
461 shadow.spread,
462 shadow.blur,
463 ),
464 ],
465 3 => vec![
466 make_shadow(
467 BLUE.into(),
468 shadow.x_offset,
469 shadow.y_offset,
470 shadow.spread,
471 shadow.blur,
472 ),
473 make_shadow(
474 YELLOW.into(),
475 -shadow.x_offset,
476 -shadow.y_offset,
477 shadow.spread,
478 shadow.blur,
479 ),
480 make_shadow(
481 RED.into(),
482 shadow.y_offset,
483 -shadow.x_offset,
484 shadow.spread,
485 shadow.blur,
486 ),
487 ],
488 _ => vec![],
489 }
490}
491
492fn make_shadow(color: Color, x_offset: f32, y_offset: f32, spread: f32, blur: f32) -> ShadowStyle {
493 ShadowStyle {
494 color: color.with_alpha(0.8),
495 x_offset: px(x_offset),
496 y_offset: px(y_offset),
497 spread_radius: px(spread),
498 blur_radius: px(blur),
499 }
500}
501
502fn update_shape(
504 shape: Res<ShapeSettings>,
505 mut query: Query<&mut Node, With<ShadowNode>>,
506 mut label_query: Query<(&mut Text, &SettingType)>,
507) {
508 for mut node in &mut query {
509 SHAPES[shape.index % SHAPES.len()].1(&mut node);
510 }
511 for (mut text, kind) in &mut label_query {
512 if *kind == SettingType::Shape {
513 *text = Text::new(SHAPES[shape.index % SHAPES.len()].0);
514 }
515 }
516}
517
518fn button_system(
520 mut interaction_query: Query<
521 (&Interaction, &SettingsButton),
522 (Changed<Interaction>, With<Button>),
523 >,
524 mut shadow: ResMut<ShadowSettings>,
525 mut shape: ResMut<ShapeSettings>,
526 mut held: ResMut<HeldButton>,
527 time: Res<Time>,
528) {
529 let now = time.elapsed_secs_f64();
530 for (interaction, btn) in &mut interaction_query {
531 match *interaction {
532 Interaction::Pressed => {
533 trigger_button_action(btn, &mut shadow, &mut shape);
534 held.button = Some(*btn);
535 held.pressed_at = Some(now);
536 held.last_repeat = Some(now);
537 }
538 Interaction::None | Interaction::Hovered => {
539 if held.button == Some(*btn) {
540 held.button = None;
541 held.pressed_at = None;
542 held.last_repeat = None;
543 }
544 }
545 }
546 }
547}
548
549fn trigger_button_action(
550 btn: &SettingsButton,
551 shadow: &mut ShadowSettings,
552 shape: &mut ShapeSettings,
553) {
554 match btn {
555 SettingsButton::XOffsetInc => shadow.x_offset += 1.0,
556 SettingsButton::XOffsetDec => shadow.x_offset -= 1.0,
557 SettingsButton::YOffsetInc => shadow.y_offset += 1.0,
558 SettingsButton::YOffsetDec => shadow.y_offset -= 1.0,
559 SettingsButton::BlurInc => shadow.blur = (shadow.blur + 1.0).max(0.0),
560 SettingsButton::BlurDec => shadow.blur = (shadow.blur - 1.0).max(0.0),
561 SettingsButton::SpreadInc => shadow.spread += 1.0,
562 SettingsButton::SpreadDec => shadow.spread -= 1.0,
563 SettingsButton::CountInc => {
564 if shadow.count < 3 {
565 shadow.count += 1;
566 }
567 }
568 SettingsButton::CountDec => {
569 if shadow.count > 1 {
570 shadow.count -= 1;
571 }
572 }
573 SettingsButton::ShapePrev => {
574 if shape.index == 0 {
575 shape.index = SHAPES.len() - 1;
576 } else {
577 shape.index -= 1;
578 }
579 }
580 SettingsButton::ShapeNext => {
581 shape.index = (shape.index + 1) % SHAPES.len();
582 }
583 SettingsButton::Reset => {
584 *shape = SHAPE_DEFAULT_SETTINGS;
585 *shadow = SHADOW_DEFAULT_SETTINGS;
586 }
587 SettingsButton::SamplesInc => shadow.samples += 1,
588 SettingsButton::SamplesDec => {
589 if shadow.samples > 1 {
590 shadow.samples -= 1;
591 }
592 }
593 }
594}
595
596fn button_repeat_system(
598 time: Res<Time>,
599 mut held: ResMut<HeldButton>,
600 mut shadow: ResMut<ShadowSettings>,
601 mut shape: ResMut<ShapeSettings>,
602 mut request_redraw_writer: MessageWriter<RequestRedraw>,
603) {
604 if held.button.is_some() {
605 request_redraw_writer.write(RequestRedraw);
606 }
607 const INITIAL_DELAY: f64 = 0.15;
608 const REPEAT_RATE: f64 = 0.08;
609 if let (Some(btn), Some(pressed_at)) = (held.button, held.pressed_at) {
610 let now = time.elapsed_secs_f64();
611 let since_pressed = now - pressed_at;
612 let last_repeat = held.last_repeat.unwrap_or(pressed_at);
613 let since_last = now - last_repeat;
614 if since_pressed > INITIAL_DELAY && since_last > REPEAT_RATE {
615 trigger_button_action(&btn, &mut shadow, &mut shape);
616 held.last_repeat = Some(now);
617 }
618 }
619}
620
621fn button_color_system(
623 mut query: Query<
624 (&Interaction, &mut BackgroundColor),
625 (Changed<Interaction>, With<Button>, With<SettingsButton>),
626 >,
627) {
628 for (interaction, mut color) in &mut query {
629 match *interaction {
630 Interaction::Pressed => *color = PRESSED_BUTTON.into(),
631 Interaction::Hovered => *color = HOVERED_BUTTON.into(),
632 Interaction::None => *color = NORMAL_BUTTON.into(),
633 }
634 }
635}