This example demonstrates how to save assets in the common case where the asset contains no subassets.

use bevy::{
    asset::{
        saver::{save_using_saver, SavedAsset},
        RenderAssetUsages,
    },
    camera::ScalingMode,
    color::palettes::tailwind,
    image::{ImageLoaderSettings, ImageSampler, ImageSaver, ImageSaverSettings},
    input::common_conditions::input_just_pressed,
    picking::pointer::Location,
    prelude::*,
    render::render_resource::{Extent3d, TextureDimension, TextureFormat},
    sprite::Anchor,
    tasks::IoTaskPool,
};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(AssetPlugin {
            // This is just overriding the default asset paths to scope this to the correct example
            // folder. You can generally skip this in your own projects.
            file_path: "examples/asset/saved_assets".to_string(),
            ..Default::default()
        }))
        .add_plugins(image_drawing_plugin)
        .add_systems(
            PreUpdate,
            perform_save.run_if(input_just_pressed(KeyCode::F5)),
        )
        .run();
}

const ASSET_PATH: &str = "art_project.png";

fn perform_save(
    image_to_save: Res<ImageToSave>,
    images: Res<Assets<Image>>,
    asset_server: Res<AssetServer>,
) {
    let image = images.get(&image_to_save.0).unwrap();

    let image = image.clone();
    let asset_server = asset_server.clone();
    IoTaskPool::get()
        .spawn(async move {
            match save_using_saver(
                asset_server.clone(),
                &ImageSaver,
                &ASSET_PATH.into(),
                SavedAsset::from_asset(&image),
                &ImageSaverSettings::default(),
            )
            .await
            {
                Ok(()) => info!("Completed save of {ASSET_PATH}"),
                Err(err) => error!("Failed to save asset: {err}"),
            }
        })
        .detach();
}

/// Plugin for doing image drawing.
///
/// This doesn't really have anything to do with asset saving, but provides a real-use case.
fn image_drawing_plugin(app: &mut App) {
    app.add_systems(Startup, setup)
        .add_observer(on_drag_start)
        .add_observer(on_drag)
        .add_observer(try_plot)
        .init_resource::<DrawColor>()
        .add_observer(on_enter_selectable)
        .add_observer(on_leave_selectable)
        .add_observer(on_press_selectable);
}

#[derive(Resource)]
struct ImageToSave(Handle<Image>);

#[derive(Component)]
struct SpriteToSave;

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut images: ResMut<Assets<Image>>,
) {
    commands.spawn((
        Camera2d,
        Projection::Orthographic(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 125.0,
            },
            ..OrthographicProjection::default_2d()
        }),
    ));

    commands.spawn(Text(
        r"Select a color from the palette at the bottom
LMB - Draw with selected color
F5 - Save image"
            .into(),
    ));

    let handle = asset_server
        .load_builder()
        .with_settings(|settings: &mut ImageLoaderSettings| {
            settings.sampler = ImageSampler::nearest();
        })
        .load(ASSET_PATH);
    commands.spawn((
        Sprite {
            image: handle.clone(),
            ..Default::default()
        },
        SpriteToSave,
        Pickable::default(),
    ));

    // We're doing something a little cursed here: we initiate a load, and then insert a default
    // image into that handle. If the load succeeds, the image will be replaced with the loaded
    // contents. If it fails, the default image will remain. In real code, you likely want to poll
    // `AssetServer::load_state` and only insert this on load failure.
    images
        .insert(&handle, {
            let mut image = Image::new_fill(
                Extent3d {
                    width: 100,
                    height: 100,
                    depth_or_array_layers: 1,
                },
                TextureDimension::D2,
                &[0, 0, 0, 255],
                TextureFormat::Rgba8Unorm,
                RenderAssetUsages::all(),
            );
            image.sampler = ImageSampler::nearest();
            image
        })
        .unwrap();

    commands.insert_resource(ImageToSave(handle));

    let container = commands
        .spawn((
            Node {
                width: percent(100),
                height: percent(100),
                align_items: AlignItems::End,
                justify_content: JustifyContent::Center,
                ..Default::default()
            },
            Pickable::IGNORE,
        ))
        .id();

    for color in [
        Color::WHITE,
        Color::Srgba(tailwind::RED_500),
        Color::Srgba(tailwind::ORANGE_500),
        Color::Srgba(tailwind::YELLOW_500),
        Color::Srgba(tailwind::GREEN_500),
        Color::Srgba(tailwind::BLUE_500),
        Color::Srgba(tailwind::INDIGO_500),
        Color::Srgba(tailwind::VIOLET_500),
        Color::BLACK,
    ] {
        let mut entity = commands.spawn((
            Node {
                width: vw(5),
                height: vh(5),
                border: px(5).all(),
                ..Default::default()
            },
            SelectableColor,
            BackgroundColor(color),
            BorderColor::all(NORMAL_COLOR),
            ChildOf(container),
        ));
        if color == Color::WHITE {
            entity.insert((Selected, BorderColor::all(SELECTED_COLOR)));
        }
    }
}

#[derive(EntityEvent)]
struct TryPlot {
    entity: Entity,
    location: Location,
}

fn on_drag_start(event: On<Pointer<DragStart>>, mut commands: Commands) {
    commands.trigger(TryPlot {
        entity: event.entity,
        location: event.pointer_location.clone(),
    });
}

fn on_drag(event: On<Pointer<Drag>>, mut commands: Commands) {
    commands.trigger(TryPlot {
        entity: event.entity,
        location: event.pointer_location.clone(),
    });
}

fn try_plot(
    event: On<TryPlot>,
    sprite: Query<(&Sprite, &Anchor, &GlobalTransform), With<SpriteToSave>>,
    camera: Single<(&Camera, &GlobalTransform)>,
    texture_atlases: Res<Assets<TextureAtlasLayout>>,
    draw_color: Res<DrawColor>,
    mut images: ResMut<Assets<Image>>,
) {
    let Ok((sprite, anchor, sprite_transform)) = sprite.get(event.entity) else {
        return;
    };
    let (camera, camera_transform) = camera.into_inner();
    let Ok(world_position) = camera.viewport_to_world_2d(camera_transform, event.location.position)
    else {
        return;
    };
    let relative_to_sprite = sprite_transform
        .affine()
        .inverse()
        .transform_point3(world_position.extend(0.0));
    let Ok(pixel_space) = sprite.compute_pixel_space_point(
        relative_to_sprite.xy(),
        *anchor,
        &images,
        &texture_atlases,
    ) else {
        return;
    };
    let pixel_coordinates = pixel_space.floor().as_uvec2();
    let mut image = images.get_mut(&sprite.image).unwrap();
    // For an actual drawing app, you'd at least draw a line from the last point, but this is
    // simpler.
    image
        .set_color_at(pixel_coordinates.x, pixel_coordinates.y, draw_color.0)
        .unwrap();
}

#[derive(Resource, Default)]
struct DrawColor(Color);

#[derive(Component)]
struct SelectableColor;

#[derive(Component)]
struct Selected;

const NORMAL_COLOR: Color = Color::BLACK;
const HIGHLIGHT_COLOR: Color = Color::Srgba(tailwind::NEUTRAL_500);
const SELECTED_COLOR: Color = Color::Srgba(tailwind::RED_600);

fn on_enter_selectable(
    event: On<Pointer<Enter>>,
    mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
) {
    let Ok(mut border) = border.get_mut(event.entity) else {
        return;
    };

    *border = BorderColor::all(HIGHLIGHT_COLOR);
}

fn on_leave_selectable(
    event: On<Pointer<Leave>>,
    mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
) {
    let Ok(mut border) = border.get_mut(event.entity) else {
        return;
    };

    *border = BorderColor::all(NORMAL_COLOR);
}

fn on_press_selectable(
    event: On<Pointer<Press>>,
    mut borders: Query<(Entity, &mut BorderColor, &BackgroundColor), With<SelectableColor>>,
    mut draw_color: ResMut<DrawColor>,
    mut commands: Commands,
) {
    if !borders.contains(event.entity) {
        return;
    }
    for (entity, mut border, _) in borders.iter_mut() {
        commands.entity(entity).remove::<Selected>();
        *border = BorderColor::all(NORMAL_COLOR);
    }
    let (_, mut border, background_color) = borders.get_mut(event.entity).unwrap();
    *border = BorderColor::all(SELECTED_COLOR);
    commands.entity(event.entity).insert(Selected);

    draw_color.0 = background_color.0;
}