1use argh::FromArgs;
4use bevy::{
5 color::palettes::css::ORANGE_RED,
6 diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
7 prelude::*,
8 text::TextColor,
9 window::{PresentMode, WindowResolution},
10 winit::WinitSettings,
11};
12
13const FONT_SIZE: f32 = 7.0;
14
15#[derive(FromArgs, Resource)]
16struct Args {
18 #[argh(switch)]
20 text: bool,
21
22 #[argh(switch)]
24 no_borders: bool,
25
26 #[argh(switch)]
28 relayout: bool,
29
30 #[argh(switch)]
32 recompute_text: bool,
33
34 #[argh(option, default = "110")]
36 buttons: usize,
37
38 #[argh(option, default = "4")]
40 image_freq: usize,
41
42 #[argh(switch)]
44 grid: bool,
45
46 #[argh(switch)]
48 respawn: bool,
49
50 #[argh(switch)]
52 display_none: bool,
53
54 #[argh(switch)]
56 no_camera: bool,
57
58 #[argh(switch)]
60 many_cameras: bool,
61}
62
63fn main() {
65 #[cfg(not(target_arch = "wasm32"))]
67 let args: Args = argh::from_env();
68 #[cfg(target_arch = "wasm32")]
69 let args = Args::from_args(&[], &[]).unwrap();
70
71 warn!(include_str!("warning_string.txt"));
72
73 let mut app = App::new();
74
75 app.add_plugins((
76 DefaultPlugins.set(WindowPlugin {
77 primary_window: Some(Window {
78 present_mode: PresentMode::AutoNoVsync,
79 resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
80 ..default()
81 }),
82 ..default()
83 }),
84 FrameTimeDiagnosticsPlugin::default(),
85 LogDiagnosticsPlugin::default(),
86 ))
87 .insert_resource(WinitSettings::continuous())
88 .add_systems(Update, (button_system, set_text_colors_changed));
89
90 if !args.no_camera {
91 app.add_systems(Startup, |mut commands: Commands| {
92 commands.spawn(Camera2d);
93 });
94 }
95
96 if args.many_cameras {
97 app.add_systems(Startup, setup_many_cameras);
98 } else if args.grid {
99 app.add_systems(Startup, setup_grid);
100 } else {
101 app.add_systems(Startup, setup_flex);
102 }
103
104 if args.relayout {
105 app.add_systems(Update, |mut nodes: Query<&mut Node>| {
106 nodes.iter_mut().for_each(|mut node| node.set_changed());
107 });
108 }
109
110 if args.recompute_text {
111 app.add_systems(Update, |mut text_query: Query<&mut Text>| {
112 text_query
113 .iter_mut()
114 .for_each(|mut text| text.set_changed());
115 });
116 }
117
118 if args.respawn {
119 if args.grid {
120 app.add_systems(Update, (despawn_ui, setup_grid).chain());
121 } else {
122 app.add_systems(Update, (despawn_ui, setup_flex).chain());
123 }
124 }
125
126 app.insert_resource(args).run();
127}
128
129fn set_text_colors_changed(mut colors: Query<&mut TextColor>) {
130 for mut text_color in colors.iter_mut() {
131 text_color.set_changed();
132 }
133}
134
135#[derive(Component)]
136struct IdleColor(Color);
137
138fn button_system(
139 mut interaction_query: Query<
140 (&Interaction, &mut BackgroundColor, &IdleColor),
141 Changed<Interaction>,
142 >,
143) {
144 for (interaction, mut color, &IdleColor(idle_color)) in interaction_query.iter_mut() {
145 *color = match interaction {
146 Interaction::Hovered => ORANGE_RED.into(),
147 _ => idle_color.into(),
148 };
149 }
150}
151
152fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
153 let images = if 0 < args.image_freq {
154 Some(vec![
155 asset_server.load("branding/icon.png"),
156 asset_server.load("textures/Game Icons/wrench.png"),
157 ])
158 } else {
159 None
160 };
161
162 let buttons_f = args.buttons as f32;
163 let border = if args.no_borders {
164 UiRect::ZERO
165 } else {
166 UiRect::all(vmin(0.05 * 90. / buttons_f))
167 };
168
169 let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
170 commands
171 .spawn(Node {
172 display: if args.display_none {
173 Display::None
174 } else {
175 Display::Flex
176 },
177 flex_direction: FlexDirection::Column,
178 justify_content: JustifyContent::Center,
179 align_items: AlignItems::Center,
180 width: percent(100),
181 height: percent(100),
182 ..default()
183 })
184 .with_children(|commands| {
185 for column in 0..args.buttons {
186 commands.spawn(Node::default()).with_children(|commands| {
187 for row in 0..args.buttons {
188 let color = as_rainbow(row % column.max(1));
189 let border_color = Color::WHITE.with_alpha(0.5).into();
190 spawn_button(
191 commands,
192 color,
193 buttons_f,
194 column,
195 row,
196 args.text,
197 border,
198 border_color,
199 images.as_ref().map(|images| {
200 images[((column + row) / args.image_freq) % images.len()].clone()
201 }),
202 );
203 }
204 });
205 }
206 });
207}
208
209fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
210 let images = if 0 < args.image_freq {
211 Some(vec![
212 asset_server.load("branding/icon.png"),
213 asset_server.load("textures/Game Icons/wrench.png"),
214 ])
215 } else {
216 None
217 };
218
219 let buttons_f = args.buttons as f32;
220 let border = if args.no_borders {
221 UiRect::ZERO
222 } else {
223 UiRect::all(vmin(0.05 * 90. / buttons_f))
224 };
225
226 let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
227 commands
228 .spawn(Node {
229 display: if args.display_none {
230 Display::None
231 } else {
232 Display::Grid
233 },
234 width: percent(100),
235 height: percent(100),
236 grid_template_columns: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
237 grid_template_rows: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
238 ..default()
239 })
240 .with_children(|commands| {
241 for column in 0..args.buttons {
242 for row in 0..args.buttons {
243 let color = as_rainbow(row % column.max(1));
244 let border_color = Color::WHITE.with_alpha(0.5).into();
245 spawn_button(
246 commands,
247 color,
248 buttons_f,
249 column,
250 row,
251 args.text,
252 border,
253 border_color,
254 images.as_ref().map(|images| {
255 images[((column + row) / args.image_freq) % images.len()].clone()
256 }),
257 );
258 }
259 }
260 });
261}
262
263fn spawn_button(
264 commands: &mut ChildSpawnerCommands,
265 background_color: Color,
266 buttons: f32,
267 column: usize,
268 row: usize,
269 spawn_text: bool,
270 border: UiRect,
271 border_color: BorderColor,
272 image: Option<Handle<Image>>,
273) {
274 let width = vw(90.0 / buttons);
275 let height = vh(90.0 / buttons);
276 let margin = UiRect::axes(width * 0.05, height * 0.05);
277 let mut builder = commands.spawn((
278 Button,
279 Node {
280 width,
281 height,
282 margin,
283 align_items: AlignItems::Center,
284 justify_content: JustifyContent::Center,
285 border,
286 ..default()
287 },
288 BackgroundColor(background_color),
289 border_color,
290 IdleColor(background_color),
291 ));
292
293 if let Some(image) = image {
294 builder.insert(ImageNode::new(image));
295 }
296
297 if spawn_text {
298 builder.with_children(|parent| {
299 parent
301 .spawn((
302 Text(format!("{column}, ")),
303 TextFont {
304 font_size: FONT_SIZE,
305 ..default()
306 },
307 TextColor(Color::srgb(0.5, 0.2, 0.2)),
308 ))
309 .with_child((
310 TextSpan(format!("{row}")),
311 TextFont {
312 font_size: FONT_SIZE,
313 ..default()
314 },
315 TextColor(Color::srgb(0.2, 0.2, 0.5)),
316 ));
317 });
318 }
319}
320
321fn despawn_ui(mut commands: Commands, root_node: Single<Entity, (With<Node>, Without<ChildOf>)>) {
322 commands.entity(*root_node).despawn();
323}
324
325fn setup_many_cameras(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
326 let images = if 0 < args.image_freq {
327 Some(vec![
328 asset_server.load("branding/icon.png"),
329 asset_server.load("textures/Game Icons/wrench.png"),
330 ])
331 } else {
332 None
333 };
334
335 let buttons_f = args.buttons as f32;
336 let border = if args.no_borders {
337 UiRect::ZERO
338 } else {
339 UiRect::all(vmin(0.05 * 90. / buttons_f))
340 };
341
342 let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
343 for column in 0..args.buttons {
344 for row in 0..args.buttons {
345 let color = as_rainbow(row % column.max(1));
346 let border_color = Color::WHITE.with_alpha(0.5).into();
347 let camera = commands
348 .spawn((
349 Camera2d,
350 Camera {
351 order: (column * args.buttons + row) as isize + 1,
352 ..Default::default()
353 },
354 ))
355 .id();
356 commands
357 .spawn((
358 Node {
359 display: if args.display_none {
360 Display::None
361 } else {
362 Display::Flex
363 },
364 flex_direction: FlexDirection::Column,
365 justify_content: JustifyContent::Center,
366 align_items: AlignItems::Center,
367 width: percent(100),
368 height: percent(100),
369 ..default()
370 },
371 UiTargetCamera(camera),
372 ))
373 .with_children(|commands| {
374 commands
375 .spawn(Node {
376 position_type: PositionType::Absolute,
377 top: vh(column as f32 * 100. / buttons_f),
378 left: vw(row as f32 * 100. / buttons_f),
379 ..Default::default()
380 })
381 .with_children(|commands| {
382 spawn_button(
383 commands,
384 color,
385 buttons_f,
386 column,
387 row,
388 args.text,
389 border,
390 border_color,
391 images.as_ref().map(|images| {
392 images[((column + row) / args.image_freq) % images.len()]
393 .clone()
394 }),
395 );
396 });
397 });
398 }
399 }
400}