1use bevy::{
10 app::AppExit,
11 input::common_conditions::{input_just_pressed, input_just_released},
12 prelude::*,
13 window::{CursorOptions, PrimaryWindow, WindowLevel},
14};
15
16#[cfg(target_os = "macos")]
17use bevy::window::CompositeAlphaMode;
18
19fn main() {
20 App::new()
21 .add_plugins(DefaultPlugins.set(WindowPlugin {
22 primary_window: Some(Window {
23 title: "Bevy Desk Toy".into(),
24 transparent: true,
25 #[cfg(target_os = "macos")]
26 composite_alpha_mode: CompositeAlphaMode::PostMultiplied,
27 ..default()
28 }),
29 ..default()
30 }))
31 .insert_resource(ClearColor(WINDOW_CLEAR_COLOR))
32 .insert_resource(WindowTransparency(false))
33 .insert_resource(CursorWorldPos(None))
34 .add_systems(Startup, setup)
35 .add_systems(
36 Update,
37 (
38 get_cursor_world_pos,
39 update_cursor_hit_test,
40 (
41 start_drag.run_if(input_just_pressed(MouseButton::Left)),
42 end_drag.run_if(input_just_released(MouseButton::Left)),
43 drag.run_if(resource_exists::<DragOperation>),
44 quit.run_if(input_just_pressed(MouseButton::Right)),
45 toggle_transparency.run_if(input_just_pressed(KeyCode::Space)),
46 move_pupils.after(drag),
47 ),
48 )
49 .chain(),
50 )
51 .run();
52}
53
54#[derive(Resource)]
56struct WindowTransparency(bool);
57
58#[derive(Resource)]
60struct CursorWorldPos(Option<Vec2>);
61
62#[derive(Resource)]
64struct DragOperation(Vec2);
65
66#[derive(Component)]
68struct InstructionsText;
69
70#[derive(Component)]
72struct BevyLogo;
73
74#[derive(Component)]
76struct Pupil {
77 eye_radius: f32,
79 pupil_radius: f32,
81 velocity: Vec2,
83}
84
85const BEVY_LOGO_RADIUS: f32 = 128.0;
88const BIRDS_EYES: [(f32, f32, f32); 3] = [
91 (145.0 - 128.0, -(56.0 - 128.0), 12.0),
92 (198.0 - 128.0, -(87.0 - 128.0), 10.0),
93 (222.0 - 128.0, -(140.0 - 128.0), 8.0),
94];
95
96const WINDOW_CLEAR_COLOR: Color = Color::srgb(0.2, 0.2, 0.2);
97
98fn setup(
100 mut commands: Commands,
101 asset_server: Res<AssetServer>,
102 mut meshes: ResMut<Assets<Mesh>>,
103 mut materials: ResMut<Assets<ColorMaterial>>,
104) {
105 commands.spawn(Camera2d);
107
108 let font = asset_server.load("fonts/FiraSans-Bold.ttf");
110 let text_style = TextFont {
111 font: font.clone(),
112 font_size: 25.0,
113 ..default()
114 };
115 commands.spawn((
116 Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."),
117 text_style.clone(),
118 Transform::from_xyz(0.0, -300.0, 100.0),
119 InstructionsText,
120 ));
121
122 let circle = meshes.add(Circle { radius: 1.0 });
124 let outline_material = materials.add(Color::BLACK);
126 let sclera_material = materials.add(Color::WHITE);
127 let pupil_material = materials.add(Color::srgb(0.2, 0.2, 0.2));
128 let pupil_highlight_material = materials.add(Color::srgba(1.0, 1.0, 1.0, 0.2));
129
130 commands
132 .spawn((
133 Sprite::from_image(asset_server.load("branding/icon.png")),
134 BevyLogo,
135 ))
136 .with_children(|commands| {
137 for (x, y, radius) in BIRDS_EYES {
139 let pupil_radius = radius * 0.6;
140 let pupil_highlight_radius = radius * 0.3;
141 let pupil_highlight_offset = radius * 0.3;
142 commands.spawn((
144 Mesh2d(circle.clone()),
145 MeshMaterial2d(outline_material.clone()),
146 Transform::from_xyz(x, y - 1.0, 1.0)
147 .with_scale(Vec2::splat(radius + 2.0).extend(1.0)),
148 ));
149
150 commands.spawn((
152 Transform::from_xyz(x, y, 2.0),
153 Visibility::default(),
154 children![
155 (
157 Mesh2d(circle.clone()),
158 MeshMaterial2d(sclera_material.clone()),
159 Transform::from_scale(Vec3::new(radius, radius, 0.0)),
160 ),
161 (
163 Transform::from_xyz(0.0, 0.0, 1.0),
164 Visibility::default(),
165 Pupil {
166 eye_radius: radius,
167 pupil_radius,
168 velocity: Vec2::ZERO,
169 },
170 children![
171 (
173 Mesh2d(circle.clone()),
174 MeshMaterial2d(pupil_material.clone()),
175 Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new(
176 pupil_radius,
177 pupil_radius,
178 1.0,
179 )),
180 ),
181 (
183 Mesh2d(circle.clone()),
184 MeshMaterial2d(pupil_highlight_material.clone()),
185 Transform::from_xyz(
186 -pupil_highlight_offset,
187 pupil_highlight_offset,
188 1.0,
189 )
190 .with_scale(Vec3::new(
191 pupil_highlight_radius,
192 pupil_highlight_radius,
193 1.0,
194 )),
195 )
196 ],
197 )
198 ],
199 ));
200 }
201 });
202}
203
204fn get_cursor_world_pos(
206 mut cursor_world_pos: ResMut<CursorWorldPos>,
207 primary_window: Single<&Window, With<PrimaryWindow>>,
208 q_camera: Single<(&Camera, &GlobalTransform)>,
209) {
210 let (main_camera, main_camera_transform) = *q_camera;
211 cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| {
213 main_camera
214 .viewport_to_world_2d(main_camera_transform, cursor_pos)
215 .ok()
216 });
217}
218
219fn update_cursor_hit_test(
221 cursor_world_pos: Res<CursorWorldPos>,
222 primary_window: Single<(&Window, &mut CursorOptions), With<PrimaryWindow>>,
223 bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
224) {
225 let (window, mut cursor_options) = primary_window.into_inner();
226 if window.decorations {
228 cursor_options.hit_test = true;
229 return;
230 }
231
232 let Some(cursor_world_pos) = cursor_world_pos.0 else {
234 return;
235 };
236
237 cursor_options.hit_test = bevy_logo_transform
239 .translation
240 .truncate()
241 .distance(cursor_world_pos)
242 < BEVY_LOGO_RADIUS;
243}
244
245fn start_drag(
247 mut commands: Commands,
248 cursor_world_pos: Res<CursorWorldPos>,
249 bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
250) {
251 let Some(cursor_world_pos) = cursor_world_pos.0 else {
253 return;
254 };
255
256 let drag_offset = bevy_logo_transform.translation.truncate() - cursor_world_pos;
258
259 if drag_offset.length() < BEVY_LOGO_RADIUS {
261 commands.insert_resource(DragOperation(drag_offset));
262 }
263}
264
265fn end_drag(mut commands: Commands) {
267 commands.remove_resource::<DragOperation>();
268}
269
270fn drag(
272 drag_offset: Res<DragOperation>,
273 cursor_world_pos: Res<CursorWorldPos>,
274 time: Res<Time>,
275 mut bevy_transform: Single<&mut Transform, With<BevyLogo>>,
276 mut q_pupils: Query<&mut Pupil>,
277) {
278 let Some(cursor_world_pos) = cursor_world_pos.0 else {
280 return;
281 };
282
283 let new_translation = cursor_world_pos + drag_offset.0;
285
286 let drag_velocity =
288 (new_translation - bevy_transform.translation.truncate()) / time.delta_secs();
289
290 bevy_transform.translation = new_translation.extend(bevy_transform.translation.z);
292
293 for mut pupil in &mut q_pupils {
297 pupil.velocity -= drag_velocity;
298 }
299}
300
301fn quit(
303 cursor_world_pos: Res<CursorWorldPos>,
304 mut app_exit: MessageWriter<AppExit>,
305 bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
306) {
307 let Some(cursor_world_pos) = cursor_world_pos.0 else {
309 return;
310 };
311
312 if bevy_logo_transform
314 .translation
315 .truncate()
316 .distance(cursor_world_pos)
317 < BEVY_LOGO_RADIUS
318 {
319 app_exit.write(AppExit::Success);
320 }
321}
322
323fn toggle_transparency(
325 mut commands: Commands,
326 mut window_transparency: ResMut<WindowTransparency>,
327 mut q_instructions_text: Query<&mut Visibility, With<InstructionsText>>,
328 mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
329) {
330 window_transparency.0 = !window_transparency.0;
332
333 for mut visibility in &mut q_instructions_text {
335 *visibility = if window_transparency.0 {
336 Visibility::Hidden
337 } else {
338 Visibility::Visible
339 };
340 }
341
342 let clear_color;
345 (
346 primary_window.decorations,
347 primary_window.window_level,
348 clear_color,
349 ) = if window_transparency.0 {
350 (false, WindowLevel::AlwaysOnTop, Color::NONE)
351 } else {
352 (true, WindowLevel::Normal, WINDOW_CLEAR_COLOR)
353 };
354
355 commands.insert_resource(ClearColor(clear_color));
357}
358
359fn move_pupils(time: Res<Time>, mut q_pupils: Query<(&mut Pupil, &mut Transform)>) {
361 for (mut pupil, mut transform) in &mut q_pupils {
362 let wiggle_radius = pupil.eye_radius - pupil.pupil_radius;
364 let z = transform.translation.z;
366 let mut translation = transform.translation.truncate();
368 pupil.velocity *= ops::powf(0.04f32, time.delta_secs());
370 translation += pupil.velocity * time.delta_secs();
372 if translation.length() > wiggle_radius {
375 translation = translation.normalize() * wiggle_radius;
376 pupil.velocity *= -0.75;
378 }
379 transform.translation = translation.extend(z);
381 }
382}