use bevy::{
app::AppExit,
input::common_conditions::{input_just_pressed, input_just_released},
prelude::*,
window::{CursorOptions, PrimaryWindow, WindowLevel},
};
#[cfg(target_os = "macos")]
use bevy::window::CompositeAlphaMode;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Desk Toy".into(),
transparent: true,
#[cfg(target_os = "macos")]
composite_alpha_mode: CompositeAlphaMode::PostMultiplied,
..default()
}),
..default()
}))
.insert_resource(ClearColor(WINDOW_CLEAR_COLOR))
.insert_resource(WindowTransparency(false))
.insert_resource(CursorWorldPos(None))
.add_systems(Startup, setup)
.add_systems(
Update,
(
get_cursor_world_pos,
update_cursor_hit_test,
(
start_drag.run_if(input_just_pressed(MouseButton::Left)),
end_drag.run_if(input_just_released(MouseButton::Left)),
drag.run_if(resource_exists::<DragOperation>),
quit.run_if(input_just_pressed(MouseButton::Right)),
toggle_transparency.run_if(input_just_pressed(KeyCode::Space)),
move_pupils.after(drag),
),
)
.chain(),
)
.run();
}
#[derive(Resource)]
struct WindowTransparency(bool);
#[derive(Resource)]
struct CursorWorldPos(Option<Vec2>);
#[derive(Resource)]
struct DragOperation(Vec2);
#[derive(Component)]
struct InstructionsText;
#[derive(Component)]
struct BevyLogo;
#[derive(Component)]
struct Pupil {
eye_radius: f32,
pupil_radius: f32,
velocity: Vec2,
}
const BEVY_LOGO_RADIUS: f32 = 128.0;
const BIRDS_EYES: [(f32, f32, f32); 3] = [
(145.0 - 128.0, -(56.0 - 128.0), 12.0),
(198.0 - 128.0, -(87.0 - 128.0), 10.0),
(222.0 - 128.0, -(140.0 - 128.0), 8.0),
];
const WINDOW_CLEAR_COLOR: Color = Color::srgb(0.2, 0.2, 0.2);
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn(Camera2d);
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
let text_style = TextFont {
font: font.clone(),
font_size: 25.0,
..default()
};
commands.spawn((
Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."),
text_style.clone(),
Transform::from_xyz(0.0, -300.0, 100.0),
InstructionsText,
));
let circle = meshes.add(Circle { radius: 1.0 });
let outline_material = materials.add(Color::BLACK);
let sclera_material = materials.add(Color::WHITE);
let pupil_material = materials.add(Color::srgb(0.2, 0.2, 0.2));
let pupil_highlight_material = materials.add(Color::srgba(1.0, 1.0, 1.0, 0.2));
commands
.spawn((
Sprite::from_image(asset_server.load("branding/icon.png")),
BevyLogo,
))
.with_children(|commands| {
for (x, y, radius) in BIRDS_EYES {
let pupil_radius = radius * 0.6;
let pupil_highlight_radius = radius * 0.3;
let pupil_highlight_offset = radius * 0.3;
commands.spawn((
Mesh2d(circle.clone()),
MeshMaterial2d(outline_material.clone()),
Transform::from_xyz(x, y - 1.0, 1.0)
.with_scale(Vec2::splat(radius + 2.0).extend(1.0)),
));
commands.spawn((
Transform::from_xyz(x, y, 2.0),
Visibility::default(),
children![
(
Mesh2d(circle.clone()),
MeshMaterial2d(sclera_material.clone()),
Transform::from_scale(Vec3::new(radius, radius, 0.0)),
),
(
Transform::from_xyz(0.0, 0.0, 1.0),
Visibility::default(),
Pupil {
eye_radius: radius,
pupil_radius,
velocity: Vec2::ZERO,
},
children![
(
Mesh2d(circle.clone()),
MeshMaterial2d(pupil_material.clone()),
Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new(
pupil_radius,
pupil_radius,
1.0,
)),
),
(
Mesh2d(circle.clone()),
MeshMaterial2d(pupil_highlight_material.clone()),
Transform::from_xyz(
-pupil_highlight_offset,
pupil_highlight_offset,
1.0,
)
.with_scale(Vec3::new(
pupil_highlight_radius,
pupil_highlight_radius,
1.0,
)),
)
],
)
],
));
}
});
}
fn get_cursor_world_pos(
mut cursor_world_pos: ResMut<CursorWorldPos>,
primary_window: Single<&Window, With<PrimaryWindow>>,
q_camera: Single<(&Camera, &GlobalTransform)>,
) {
let (main_camera, main_camera_transform) = *q_camera;
cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| {
main_camera
.viewport_to_world_2d(main_camera_transform, cursor_pos)
.ok()
});
}
fn update_cursor_hit_test(
cursor_world_pos: Res<CursorWorldPos>,
primary_window: Single<(&Window, &mut CursorOptions), With<PrimaryWindow>>,
bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) {
let (window, mut cursor_options) = primary_window.into_inner();
if window.decorations {
cursor_options.hit_test = true;
return;
}
let Some(cursor_world_pos) = cursor_world_pos.0 else {
return;
};
cursor_options.hit_test = bevy_logo_transform
.translation
.truncate()
.distance(cursor_world_pos)
< BEVY_LOGO_RADIUS;
}
fn start_drag(
mut commands: Commands,
cursor_world_pos: Res<CursorWorldPos>,
bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) {
let Some(cursor_world_pos) = cursor_world_pos.0 else {
return;
};
let drag_offset = bevy_logo_transform.translation.truncate() - cursor_world_pos;
if drag_offset.length() < BEVY_LOGO_RADIUS {
commands.insert_resource(DragOperation(drag_offset));
}
}
fn end_drag(mut commands: Commands) {
commands.remove_resource::<DragOperation>();
}
fn drag(
drag_offset: Res<DragOperation>,
cursor_world_pos: Res<CursorWorldPos>,
time: Res<Time>,
mut bevy_transform: Single<&mut Transform, With<BevyLogo>>,
mut q_pupils: Query<&mut Pupil>,
) {
let Some(cursor_world_pos) = cursor_world_pos.0 else {
return;
};
let new_translation = cursor_world_pos + drag_offset.0;
let drag_velocity =
(new_translation - bevy_transform.translation.truncate()) / time.delta_secs();
bevy_transform.translation = new_translation.extend(bevy_transform.translation.z);
for mut pupil in &mut q_pupils {
pupil.velocity -= drag_velocity;
}
}
fn quit(
cursor_world_pos: Res<CursorWorldPos>,
mut app_exit: EventWriter<AppExit>,
bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) {
let Some(cursor_world_pos) = cursor_world_pos.0 else {
return;
};
if bevy_logo_transform
.translation
.truncate()
.distance(cursor_world_pos)
< BEVY_LOGO_RADIUS
{
app_exit.write(AppExit::Success);
}
}
fn toggle_transparency(
mut commands: Commands,
mut window_transparency: ResMut<WindowTransparency>,
mut q_instructions_text: Query<&mut Visibility, With<InstructionsText>>,
mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
) {
window_transparency.0 = !window_transparency.0;
for mut visibility in &mut q_instructions_text {
*visibility = if window_transparency.0 {
Visibility::Hidden
} else {
Visibility::Visible
};
}
let clear_color;
(
primary_window.decorations,
primary_window.window_level,
clear_color,
) = if window_transparency.0 {
(false, WindowLevel::AlwaysOnTop, Color::NONE)
} else {
(true, WindowLevel::Normal, WINDOW_CLEAR_COLOR)
};
commands.insert_resource(ClearColor(clear_color));
}
fn move_pupils(time: Res<Time>, mut q_pupils: Query<(&mut Pupil, &mut Transform)>) {
for (mut pupil, mut transform) in &mut q_pupils {
let wiggle_radius = pupil.eye_radius - pupil.pupil_radius;
let z = transform.translation.z;
let mut translation = transform.translation.truncate();
pupil.velocity *= ops::powf(0.04f32, time.delta_secs());
translation += pupil.velocity * time.delta_secs();
if translation.length() > wiggle_radius {
translation = translation.normalize() * wiggle_radius;
pupil.velocity *= -0.75;
}
transform.translation = translation.extend(z);
}
}