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