Commit graph

675 commits

Author SHA1 Message Date
Benjamin Brienen
9386bd0114
feature gate picking backends (#15369)
# Objective

Fixes #15306

## Solution

- Add feature gate on the module and the place where each one is used
- Declare the features and make them default

## Testing

- CI
2024-09-22 19:35:15 +00:00
ickshonpe
48f2bd410b
RenderUiSystem::ExtractTextureSlice (#15332)
# Objective

Fixes #15330

## Solution
1. Add an `ExtractTextureSlice` variant to `RenderUiSystem`.
2. Add `RenderUiSystem::ExtractTextureSlice` to the `ExtractSchedule`
between `ExtractImages` and `ExtractBorders`.
3. Add `extract_ui_texture_slices` to the new `ExtractTextureSlice`
system set.

Which results in texture slice nodes being extracted before borders. No
more z-fighting, borders will always be drawn on top of texture-sliced
images.
2024-09-20 23:55:11 +00:00
Rich Churcher
fd329c0426
Allow to expect (adopted) (#15301)
# Objective

> Rust 1.81 released the #[expect(...)] attribute, which works like
#[allow(...)] but throws a warning if the lint isn't raised. This is
preferred to #[allow(...)] because it tells us when it can be removed.

- Adopts the parts of #15118 that are complete, and updates the branch
so it can be merged.
- There were a few conflicts, let me know if I misjudged any of 'em.

Alice's
[recommendation](https://github.com/bevyengine/bevy/issues/15059#issuecomment-2349263900)
seems well-taken, let's do this crate by crate now that @BD103 has done
the lion's share of this!

(Relates to, but doesn't yet completely finish #15059.)

Crates this _doesn't_ cover:

- bevy_input
- bevy_gilrs
- bevy_window
- bevy_winit
- bevy_state
- bevy_render
- bevy_picking
- bevy_core_pipeline
- bevy_sprite
- bevy_text
- bevy_pbr
- bevy_ui
- bevy_gltf
- bevy_gizmos
- bevy_dev_tools
- bevy_internal
- bevy_dylib

---------

Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com>
Co-authored-by: Ben Frankel <ben.frankel7@gmail.com>
Co-authored-by: Antony <antony.m.3012@gmail.com>
2024-09-20 19:16:42 +00:00
patrickariel
3efef59d83
Enable/disable UI anti-aliasing (#15170)
# Objective

Currently, UI is always rendered with anti-aliasing. This makes bevy's
UI completely unsuitable for art-styles that demands hard pixelated
edges, such as retro-style games.

## Solution

Add a component for disabling anti-aliasing in UI.

## Testing

In
[`examples/ui/button.rs`](15e246eff8/examples/ui/button.rs),
add the component to the camera like this:

```rust
use bevy::{prelude::*, ui::prelude::*};

commands.spawn((Camera2dBundle::default(), UiAntiAlias::Off));
```

The rounded button will now render without anti-aliasing.

## Showcase

An example of a rounded UI node rendered without anti-aliasing, with and
without borders:


![image](https://github.com/user-attachments/assets/ea797e40-bdaa-4ede-a0d3-c9a7eab95b6e)
2024-09-16 23:06:23 +00:00
Blazepaws
cb6ab16c97
Reflect derived traits on all components and resources: bevy_ui (#15231)
Solves https://github.com/bevyengine/bevy/issues/15187 for bevy_ui
2024-09-15 17:52:38 +00:00
ickshonpe
1b1105e327
Remove border radius scaling (#15173)
# Objective

Fixes #15142

Split this off from #15163 as it's a very simple fix.

## Solution

UiScale was applied twice to border radius, remove the second
application.

## Testing

You can use this modified button example from the issue for testing:

```
use bevy::{color::palettes::basic::*, prelude::*, winit::WinitSettings};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Only run the app when there is user input. This will significantly reduce CPU/GPU use.
        .insert_resource(UiScale(2.))
        .insert_resource(WinitSettings::desktop_app())
        .add_systems(Startup, setup)
        .add_systems(Update, button_system)
        .run();
}

const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);

fn button_system(
    mut interaction_query: Query<
        (
            &Interaction,
            &mut BackgroundColor,
            &mut BorderColor,
            &Children,
        ),
        (Changed<Interaction>, With<Button>),
    >,
    mut text_query: Query<&mut Text>,
) {
    for (interaction, mut color, mut border_color, children) in &mut interaction_query {
        let mut text = text_query.get_mut(children[0]).unwrap();
        match *interaction {
            Interaction::Pressed => {
                text.sections[0].value = "Press".to_string();
                *color = PRESSED_BUTTON.into();
                border_color.0 = RED.into();
            }
            Interaction::Hovered => {
                text.sections[0].value = "Hover".to_string();
                *color = HOVERED_BUTTON.into();
                border_color.0 = Color::WHITE;
            }
            Interaction::None => {
                text.sections[0].value = "Button".to_string();
                *color = NORMAL_BUTTON.into();
                border_color.0 = Color::BLACK;
            }
        }
    }
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // ui camera
    commands.spawn(Camera2dBundle::default());
    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                ..default()
            },
            ..default()
        })
        .with_children(|parent| {
            parent
                .spawn(ButtonBundle {
                    style: Style {
                        width: Val::Px(250.0),
                        height: Val::Px(100.0),
                        border: UiRect::all(Val::Px(25.0)),
                        // horizontally center child text
                        justify_content: JustifyContent::Center,
                        // vertically center child text
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    border_color: BorderColor(Color::BLACK),
                    border_radius: BorderRadius::all(Val::Px(25.)),
                    background_color: NORMAL_BUTTON.into(),
                    ..default()
                })
                .with_child(TextBundle::from_section(
                    "Button",
                    TextStyle {
                        font: asset_server.load("fonts/FiraSans-Bold.ttf"),
                        font_size: 40.0,
                        color: Color::srgb(0.9, 0.9, 0.9),
                    },
                ));
            parent
                .spawn(NodeBundle {
                    style: Style {
                        width: Val::Px(150.0),
                        padding: UiRect::vertical(Val::Px(25.)),
                        height: Val::Px(100.0),
                        align_items: AlignItems::Stretch,
                        justify_content: JustifyContent::Stretch,
                        ..Default::default()
                    },
                    ..default()
                })
                .with_child(NodeBundle {
                    style: Style {
                        flex_basis: Val::Percent(100.),
                        ..Default::default()
                    },
                    background_color: RED.into(),
                    ..Default::default()
                });
        });
}
```

## Showcase

Using the modified button example

### main

<img alt="366023197-e6124f07-e522-4514-bd8e-7986ac32890c"
src="https://github.com/user-attachments/assets/b7b909ed-1184-4d9d-b50b-e30f4c1f76b2">


### this PR

![image](https://github.com/user-attachments/assets/89b89a2f-533f-41bd-b2cb-4743aec6519e)
2024-09-13 15:52:42 +00:00
ickshonpe
cacf3929db
fix spelling mistake (#15146)
# Objective

Fix spelling mistake: `interned_root_notes` -> `interned_root_nodes`
2024-09-10 20:21:40 +00:00
ickshonpe
8d143e3ed8
ui material node border calculations fix (#15119)
# Objective

Fixes  #15115

## Solution

Retrieve the size of the node's parent in a separate query and base
percentage border values on the parent node's width (or the width of the
viewport in the case of root nodes).
2024-09-09 22:35:29 +00:00
ickshonpe
4de9edeaa6
Retrieve the stack_index from Node in extract_ui_material_nodes instead of walking UiStack (#15104)
# Objective

 `ExtractedUiMaterialNode` is still walking the whole `UiStack`. 

more info: https://github.com/bevyengine/bevy/pull/9853

## Solution

Retrieve the `stack_index` from the `Node` component instead.
Also changed the `stack_index` field of `ExtractedUiMaterialNode` to
`u32`.
2024-09-09 16:18:37 +00:00
Marco Meijer
66b5128b6f
Add rect field to UI image (#15095)
# Objective

Fixes #14424 

## Solution

Add a rect field to UiImage, and update the extraction of ui images and
slices.

## Testing

I tested all possible combinations of having a rect, using a texture
atlas, setting image scale mode to sliced and image scale mode to tiled.
See the showcase section.

---

## Showcase

<img width="1279" alt="Screenshot 2024-09-08 at 16 23 05"
src="https://github.com/user-attachments/assets/183e53eb-f27c-4c8e-9fd5-4678825db3b6">

<details>
  <summary>Click to view showcase</summary>

```rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
        .add_systems(Startup, create_ui)
        .run();
}

fn create_ui(
    mut commands: Commands,
    assets: Res<AssetServer>,
    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
    let texture = assets.load("textures/fantasy_ui_borders/numbered_slices.png");
    let layout = TextureAtlasLayout::from_grid(UVec2::splat(16), 3, 3, None, None);
    let texture_atlas_layout = texture_atlas_layouts.add(layout);

    commands.spawn(Camera2dBundle::default());

    let style = Style {
        width: Val::Px(96.),
        height: Val::Px(96.),
        ..default()
    };

    commands
        .spawn(NodeBundle { ..default() })
        .with_children(|parent| {
            // nothing
            parent.spawn(ImageBundle {
                image: UiImage::new(texture.clone()),
                style: style.clone(),
                ..default()
            });

            // with rect
            parent.spawn(ImageBundle {
                image: UiImage::new(texture.clone()).with_rect(Rect::new(0., 0., 16., 16.)),
                style: style.clone(),
                ..default()
            });

            // with rect and texture atlas
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()).with_rect(Rect::new(0., 0., 8., 8.)),
                    style: style.clone(),
                    ..default()
                },
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 1,
                },
            ));

            // with texture atlas
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()),
                    style: style.clone(),
                    ..default()
                },
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 2,
                },
            ));

            // with texture slicer
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()),
                    style: style.clone(),
                    ..default()
                },
                ImageScaleMode::Sliced(TextureSlicer {
                    border: BorderRect::square(16.),
                    center_scale_mode: SliceScaleMode::Stretch,
                    sides_scale_mode: SliceScaleMode::Stretch,
                    max_corner_scale: 1.,
                }),
            ));

            // with rect and texture slicer
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()).with_rect(Rect::new(0., 0., 16., 16.)),
                    style: style.clone(),
                    ..default()
                },
                ImageScaleMode::Sliced(TextureSlicer {
                    border: BorderRect::square(2.),
                    center_scale_mode: SliceScaleMode::Stretch,
                    sides_scale_mode: SliceScaleMode::Stretch,
                    max_corner_scale: 1.,
                }),
            ));

            // with rect, texture atlas and texture slicer
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()).with_rect(Rect::new(0., 0., 8., 8.)),
                    style: style.clone(),
                    ..default()
                },
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 1,
                },
                ImageScaleMode::Sliced(TextureSlicer {
                    border: BorderRect::square(1.),
                    center_scale_mode: SliceScaleMode::Stretch,
                    sides_scale_mode: SliceScaleMode::Stretch,
                    max_corner_scale: 1.,
                }),
            ));

            // with texture atlas and texture slicer
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()),
                    style: style.clone(),
                    ..default()
                },
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 2,
                },
                ImageScaleMode::Sliced(TextureSlicer {
                    border: BorderRect::square(2.),
                    center_scale_mode: SliceScaleMode::Stretch,
                    sides_scale_mode: SliceScaleMode::Stretch,
                    max_corner_scale: 1.,
                }),
            ));

            // with tiled
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()),
                    style: style.clone(),
                    ..default()
                },
                ImageScaleMode::Tiled {
                    tile_x: true,
                    tile_y: true,
                    stretch_value: 1.,
                },
            ));

            // with rect and tiled
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()).with_rect(Rect::new(0., 0., 16., 16.)),
                    style: style.clone(),
                    ..default()
                },
                ImageScaleMode::Tiled {
                    tile_x: true,
                    tile_y: true,
                    stretch_value: 1.,
                },
            ));

            // with rect, texture atlas and tiled
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()).with_rect(Rect::new(0., 0., 8., 8.)),
                    style: style.clone(),
                    ..default()
                },
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 1,
                },
                ImageScaleMode::Tiled {
                    tile_x: true,
                    tile_y: true,
                    stretch_value: 1.,
                },
            ));

            // with texture atlas and tiled
            parent.spawn((
                ImageBundle {
                    image: UiImage::new(texture.clone()),
                    style: style.clone(),
                    ..default()
                },
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 2,
                },
                ImageScaleMode::Tiled {
                    tile_x: true,
                    tile_y: true,
                    stretch_value: 1.,
                },
            ));
        });
}
```

</details>
2024-09-09 16:16:33 +00:00
CrazyboyQCD
bca228fdaa
Simplify pick_rounded_rect (#15065)
# Objective

Simplify `pick_rounded_rect` with multiple `if` statements to make it
more readable and efficient([Godbolt
link](https://godbolt.org/z/W5vPEvT5c)).

Co-authored-by: WX\shixi <shixi1@cnwxsoft.com>
2024-09-09 15:40:00 +00:00
Ben Frankel
7b217a976c
Remove deprecated SpriteSheetBundle and AtlasImageBundle (#15062)
# Objective

Remove bundles that were deprecated in 0.14.

## Testing

`rg SpriteSheetBundle` and `rg AtlasImageBundle` show no results.
2024-09-09 15:36:09 +00:00
BD103
6ec6a55645
Unify crate-level preludes (#15080)
# Objective

- Crate-level prelude modules, such as `bevy_ecs::prelude`, are plagued
with inconsistency! Let's fix it!

## Solution

Format all preludes based on the following rules:

1. All preludes should have brief documentation in the format of:
   > The _name_ prelude.
   >
> This includes the most common types in this crate, re-exported for
your convenience.
2. All documentation should be outer, not inner. (`///` instead of
`//!`.)
3. No prelude modules should be annotated with `#[doc(hidden)]`. (Items
within them may, though I'm not sure why this was done.)

## Testing

- I manually searched for the term `mod prelude` and updated all
occurrences by hand. 🫠

---------

Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
2024-09-08 17:10:57 +00:00
BigWingBeat
54aa45e62f
Fix bevy_ui compile error when bevy_picking feature is disabled (#15053)
# Objective

#14957 added the `pick_rounded_rect` function to `bevy_ui` in the
`picking_backend` module, which is gated behind the `bevy_picking`
feature. This function is used in that module, as well as in the `focus`
module. The latter usage is not gated behind the `bevy_picking` feature,
causing a compile error when the feature is disabled.

## Solution

Move the `pick_rounded_rect` function out of the `picking_backend`
module, as it does not depend on anything defined in that module. I put
it in `lib.rs` but it could reasonably be moved somewhere else instead.

## Testing

Encountered this compile error in a project and confirmed that this
patch fixes it.
2024-09-05 19:16:57 +00:00
ickshonpe
cb221d8852
Node::is_empty (#15050)
# Objective

Add a `Node::is_empty` method to replace the `uinode.size().x() <= 0. ||
uinode.size.y() <= 0.` checks.
2024-09-05 16:26:45 +00:00
ickshonpe
a0f5ea0d36
UI outlines radius (#15018)
# Objective

Fixes #13479

This also fixes the gaps you can sometimes observe in outlines
(screenshot from main, not this PR):

<img width="636" alt="outline-gaps"
src="https://github.com/user-attachments/assets/c11dae24-20f5-4aea-8ffc-1894ad2a2b79">

The outline around the last item in each section has vertical gaps. 

## Solution

Draw the outlines with corner radius using the existing border rendering
for uinodes. The outline radius is very simple to calculate. We just
take the computed border radius of the node, and if it's greater than
zero, add it to the distance from the edge of the node to the outer edge
of the node's outline.

---

## Showcase

<img width="634" alt="outlines-radius"
src="https://github.com/user-attachments/assets/1ecda26c-65c5-41ef-87e4-5d9171ddc3ae">

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
2024-09-04 22:30:16 +00:00
ickshonpe
8ac745ab10
UI texture slice texture flipping reimplementation (#15034)
# Objective

Fixes #15032

## Solution

Reimplement support for the `flip_x` and `flip_y` fields.
This doesn't flip the border geometry, I'm not really sure whether that
is desirable or not.
Also fixes a bug that was causing the side and center slices to tile
incorrectly.

### Testing

```
cargo run --example ui_texture_slice_flip_and_tile
```

## Showcase
<img width="787" alt="nearest"
src="https://github.com/user-attachments/assets/bc044bae-1748-42ba-92b5-0500c87264f6">
With tiling need to use nearest filtering to avoid bleeding between the
slices.

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-09-04 19:31:41 +00:00
Alice Cecile
4ac2a63556
Remove all existing system order ambiguities in DefaultPlugins (#15031)
# Objective

As discussed in https://github.com/bevyengine/bevy/issues/7386, system
order ambiguities within `DefaultPlugins` are a source of bugs in the
engine and badly pollute diagnostic output for users.

We should eliminate them!

This PR is an alternative to #15027: with all external ambiguities
silenced, this should be much less prone to merge conflicts and the test
output should be much easier for authors to understand.

Note that system order ambiguities are still permitted in the
`RenderApp`: these need a bit of thought in terms of how to test them,
and will be fairly involved to fix. While these aren't *good*, they'll
generally only cause graphical bugs, not logic ones.

## Solution

All remaining system order ambiguities have been resolved.
Review this PR commit-by-commit to see how each of these problems were
fixed.

## Testing

`cargo run --example ambiguity_detection` passes with no panics or
logging!
2024-09-03 20:24:34 +00:00
Chris Juchem
c620eb7833
Return Results from Camera's world/viewport conversion methods (#14989)
# Objective

- Fixes https://github.com/bevyengine/bevy/issues/14593.

## Solution

- Add `ViewportConversionError` and return it from viewport conversion
methods on Camera.

## Testing

- I successfully compiled and ran all changed examples.

## Migration Guide

The following methods on `Camera` now return a `Result` instead of an
`Option` so that they can provide more information about failures:
 - `world_to_viewport`
 - `world_to_viewport_with_depth`
 - `viewport_to_world`
 - `viewport_to_world_2d`

Call `.ok()` on the `Result` to turn it back into an `Option`, or handle
the `Result` directly.

---------

Co-authored-by: Lixou <82600264+DasLixou@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Zachary Harrold <zac@harrold.com.au>
2024-09-03 19:45:15 +00:00
ickshonpe
4e9a62f094
Ignore clicks on uinodes outside of rounded corners (#14957)
# Objective

Fixes #14941

## Solution
1. Add a `resolved_border_radius` field to `Node` to hold the resolved
border radius values.
2. Remove the border radius calculations from the UI's extraction
functions.
4. Compute the border radius during UI relayouts in `ui_layout_system`
and store them in `Node`.
5. New `pick_rounded_rect` function based on the border radius SDF from
`ui.wgsl`.
6. Use `pick_rounded_rect` in `focus` and `picking_backend` to check if
the pointer is hovering UI nodes with rounded corners.
---

## Showcase

```
cargo run --example button
```


https://github.com/user-attachments/assets/ea951a64-17ef-455e-b5c9-a2e6f6360648

## Testing

Modified button example with buttons with different corner radius:

```
use bevy::{color::palettes::basic::*, prelude::*, winit::WinitSettings};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Only run the app when there is user input. This will significantly reduce CPU/GPU use.
        .insert_resource(WinitSettings::desktop_app())
        .add_systems(Startup, setup)
        .add_systems(Update, button_system)
        .run();
}

const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);

fn button_system(
    mut interaction_query: Query<
        (
            &Interaction,
            &mut BackgroundColor,
            &mut BorderColor,
            &Children,
        ),
        (Changed<Interaction>, With<Button>),
    >,
    mut text_query: Query<&mut Text>,
) {
    for (interaction, mut color, mut border_color, children) in &mut interaction_query {
        let mut text = text_query.get_mut(children[0]).unwrap();
        match *interaction {
            Interaction::Pressed => {
                text.sections[0].value = "Press".to_string();
                *color = PRESSED_BUTTON.into();
                border_color.0 = RED.into();
            }
            Interaction::Hovered => {
                text.sections[0].value = "Hover".to_string();
                *color = HOVERED_BUTTON.into();
                border_color.0 = Color::WHITE;
            }
            Interaction::None => {
                text.sections[0].value = "Button".to_string();
                *color = NORMAL_BUTTON.into();
                border_color.0 = Color::BLACK;
            }
        }
    }
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // ui camera
    commands.spawn(Camera2dBundle::default());
    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                row_gap: Val::Px(10.),
                ..default()
            },
            ..default()
        })
        .with_children(|parent| {
            for border_radius in [
                BorderRadius {
                    top_left: Val::ZERO,
                    ..BorderRadius::MAX
                },
                BorderRadius {
                    top_right: Val::ZERO,
                    ..BorderRadius::MAX
                },
                BorderRadius {
                    bottom_right: Val::ZERO,
                    ..BorderRadius::MAX
                },
                BorderRadius {
                    bottom_left: Val::ZERO,
                    ..BorderRadius::MAX
                },
            ] {
                parent
                    .spawn(ButtonBundle {
                        style: Style {
                            width: Val::Px(150.0),
                            height: Val::Px(65.0),
                            border: UiRect::all(Val::Px(5.0)),
                            // horizontally center child text
                            justify_content: JustifyContent::Center,
                            // vertically center child text
                            align_items: AlignItems::Center,
                            ..default()
                        },
                        border_color: BorderColor(Color::BLACK),
                        border_radius,
                        background_color: NORMAL_BUTTON.into(),
                        ..default()
                    })
                    .with_child(TextBundle::from_section(
                        "Button",
                        TextStyle {
                            font: asset_server.load("fonts/FiraSans-Bold.ttf"),
                            font_size: 40.0,
                            color: Color::srgb(0.9, 0.9, 0.9),
                        },
                    ));
            }
        });
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Matty <weatherleymatthew@gmail.com>
2024-09-03 12:38:59 +00:00
ickshonpe
01a3b0e830
UI texture atlas slice shader (#14990)
# Objective

Fixes https://github.com/bevyengine/bevy/issues/14183

## Solution

Reimplement the UI texture atlas slicer using a shader. 

The problems with #14183 could be fixed more simply by hacking around
with the coordinates and scaling but that way is very fragile and might
get broken again the next time we make changes to the layout
calculations. A shader based solution is more robust, it's impossible
for gaps to appear between the image slices with these changes as we're
only drawing a single quad.

I've not tried any benchmarks yet but it should much more efficient as
well, in the worst cases even hundreds or thousands of times faster.

Maybe could have used the UiMaterialPipeline. I wrote the shader first
and used fat vertices and then realised it wouldn't work that way with a
UiMaterial. If it's rewritten it so it puts all the slice geometry in
uniform buffer, then it might work? Adding the uniform buffer would
probably make the shader more complicated though, so don't know if it's
even worth it. Instancing is another alternative.

## Testing
The examples are working and it seems to match the old API correctly but
I've not used the texture atlas slicing API for anything before, I
reviewed the PR but that was back in January.

Needs a review by someone who knows the rendering pipeline and wgsl
really well because I don't really have any idea what I'm doing.
2024-09-02 23:03:58 +00:00
ickshonpe
96942058f7
Extract borders without border radius (#15020)
# Objective

The `BorderRadius` component shouldn't be required to draw borders for
nodes with sharp corners.

## Solution

Make `BorderRadius` optional in `extract_uinode_borders`'s UI node
query.
2024-09-02 22:47:43 +00:00
UkoeHB
3fc02cb925
Reduce allocations in ui_layout_system (#15001)
# Objective

- Shave off some allocations from `ui_layout_system`.

## Solution

- Add a `Local` for allocation buffers.
2024-09-02 22:37:27 +00:00
UkoeHB
3227c3de36
Don't reallocate scale factors in measure_text_system (#14999)
# Objective

- Reuse allocation of `scale_factors` in `measure_text_system`.

## Solution

- Move it to a `Local`.
2024-09-02 17:01:59 +00:00
UkoeHB
2b94a108ae
Reuse TextLayoutInfo in queue_text (#14997)
# Objective

Don't reallocate `TextLayoutInfo` every time it needs to be updated.

## Solution

Reuse existing allocation.
2024-09-02 17:01:56 +00:00
ickshonpe
be100b8760
Resolve UI outlines using the correct target's viewport size (#14947)
# Objective
`resolve_outlines_system` wasn't updated when multi-window support was
added and it always uses the size of the primary window when resolving
viewport coords, regardless of the layout's camera target.

Fixes #14945

## Solution

It's awkward to get the viewport size of the target for an individual
node without walking the tree or adding extra fields to `Node`, so I
removed `resolve_outlines_system` and instead the outline values are
updated in `ui_layout_system`.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-09-02 16:56:58 +00:00
UkoeHB
41474226c3
Optimize UI text measurement (#15003)
# Objective

- Avoid cloning the `CosmicBuffer` every time you create a new text
measurement.

## Solution

- Inject a buffer query when calculating layout so existing buffers can
be reused.

## Testing

- I tested the `text`, `text_debug`, and `text_wrap_debug` examples.
- I did not do a performance test.
2024-09-01 11:50:54 +00:00
Zachary Harrold
bc13161416
Migrated NonZero* to NonZero<*> (#14978)
# Objective

- Fixes #14974

## Solution

- Replace all* instances of `NonZero*` with `NonZero<*>`

## Testing

- CI passed locally.

---

## Notes

Within the `bevy_reflect` implementations for `std` types,
`impl_reflect_value!()` will continue to use the type aliases instead,
as it inappropriately parses the concrete type parameter as a generic
argument. If the `ZeroablePrimitive` trait was stable, or the macro
could be modified to accept a finite list of types, then we could fully
migrate.
2024-08-30 02:37:47 +00:00
François Mockers
e63d7c340f
don't use padding for layout (#14944)
# Objective

- Fixes #14792 
- Padding is already handled by taffy, don't handle it also on Bevy side

## Solution

- Remove extra computation added in
https://github.com/bevyengine/bevy/pull/14777
2024-08-27 22:41:23 +00:00
charlotte
1caa64d948
Refactor AsBindGroup to use a associated SystemParam. (#14909)
# Objective

Adding more features to `AsBindGroup` proc macro means making the trait
arguments uglier. Downstream implementors of the trait without the proc
macro might want to do different things than our default arguments.

## Solution

Make `AsBindGroup` take an associated `Param` type.

## Migration Guide

`AsBindGroup` now allows the user to specify a `SystemParam` to be used
for creating bind groups.
2024-08-25 20:16:34 +00:00
robtfm
6e2f96f222
check sampler type in as_bind_group derives (#12637)
# Objective

currently if we use an image with the wrong sampler type in a material,
wgpu panics with an invalid texture format. turn this into a warning and
fail more gracefully.

## Solution

the expected sampler type is specified in the AsBindGroup derive, so we
can just check the image sampler is what it should be.

i am not totally sure about the mapping of image sampler type to
#[sampler(type)], i assumed:

```
    "filtering" => [ TextureSampleType::Float { filterable: true } ],
    "non_filtering" => [
        TextureSampleType::Float { filterable: false },
        TextureSampleType::Sint,
        TextureSampleType::Uint,
    ],
    "comparison" => [ TextureSampleType::Depth ],
```
2024-08-21 01:41:31 +00:00
Rob Parrett
618cf7f51d
Remove useless Direction field (#14793)
# Objective

Delete some code that isn't actually doing anything. This was actually
discovered way back in this obsolete PR: #5513.

Also Fixes #6286

## Solution

Delete it

## Alternatives

Make `Direction` do things. But it's not totally clear to me if it's
possible to override cosmic-text's unicode bidi stuff.

## Migration Guide

`Style` no longer has a `direction` field, and `Direction` has been
deleted. They didn't do anything, so you can delete any references to
them as well.
2024-08-19 21:45:28 +00:00
Nihilistas
ae74df3464
#14143 - fix bevy_ui padding (#14777)
# Objective

fixes #14143

## Solution

- removed the temporary blocker if statement when setting padding in
`Style`
- adjusted the `layout_location` and `layout_size` so they use
`layout.padding` which we already get from Taffy

## Testing

- this is the test code I used:
```rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
){
    let font = asset_server.load("fonts/FiraSans-Bold.ttf");
    commands.spawn(Camera2dBundle::default());

    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Px(200.),
                height: Val::Px(100.),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                align_self: AlignSelf::Center,
                justify_self: JustifySelf::Center,
                ..Default::default()
            },
            background_color: BackgroundColor(Color::srgb(0.,1., 1.)),
            ..Default::default()
        })
        .with_children(|builder| {
            builder.spawn((TextBundle::from_section(
                    "Hello World",
                    TextStyle {
                        font,
                        font_size: 32.0,
                        color: Color::WHITE,
                        },
                ).with_style(Style {
                    padding: UiRect::all(Val::Px(10.)),
                    width: Val::Px(100.),
                    height: Val::Px(100.),
                    ..Default::default()
                }).with_background_color(Color::srgb(1.,0., 0.)),
            ));
            // spawn an image bundle
            builder.spawn(ImageBundle {
                style: Style {
                    padding: UiRect::all(Val::Px(10.)),
                    width: Val::Px(100.),
                    height: Val::Px(100.),
                    ..Default::default()
                },
                image: asset_server.load("square.png").into(),
                ..Default::default()
            });
        });
}
```

- I tested 5 cases: 10px padding from all sides, and 10px padding from
left, right, bottom, and top separately

- **For reviewers**: please check more cases or try to run it on some
more complicated real-world UI

## Showcase

<img width="374" alt="Screenshot 2024-08-16 at 09 28 04"
src="https://github.com/user-attachments/assets/59b85b00-e255-4669-be13-a287ef35d4d9">
<img width="288" alt="Screenshot 2024-08-16 at 09 28 47"
src="https://github.com/user-attachments/assets/170a79b1-ec9c-45f9-82f5-ba7fa4029334">
<img width="274" alt="Screenshot 2024-08-16 at 09 45 16"
src="https://github.com/user-attachments/assets/e3fd9b59-b41f-427d-8c07-5acdf1dc5ecf">
<img width="292" alt="Screenshot 2024-08-16 at 09 45 36"
src="https://github.com/user-attachments/assets/c4f708aa-3f0d-4ff3-b779-0d4ed5f6ba73">
<img width="261" alt="Screenshot 2024-08-16 at 09 45 58"
src="https://github.com/user-attachments/assets/eba1e26f-04ca-4178-87c8-3a79daff3a9a">

---------

Co-authored-by: dpeke <dpekelis@funstage.com>
2024-08-16 21:22:44 +00:00
Alice Cecile
5243fe6956
Remove manual apply_deferred in bevy_ui (#14768)
# Objective

This `apply_deferred` doesn't seem to have any effect, pointlessly
restricts parallelism and is responsible for a large number of system
order ambiguities. Spotted as part of #7386.

## Solution

Remove it.

This is the *only* manual apply_deferred in the code base currently.

## Testing

I've checked various UI examples and `split_screen`, and couldn't
discern any difference.

This looks like a remnant of a `(a, apply_deferred, b).chain()` pattern
where `b` got removed, leaving us with a weird vestige.
2024-08-15 20:51:25 +00:00
TotalKrill
6adf31babf
hooking up observers and clicking for ui node (#14695)
Makes the newly merged picking usable for UI elements. 

currently it both triggers the events, as well as sends them as throught
commands.trigger_targets. We should probably figure out if this is
needed for them all.

# Objective

Hooks up obserers and picking for a very simple example

## Solution

upstreamed the UI picking backend from bevy_mod_picking

## Testing

tested with the new example picking/simple_picking.rs


---

---------

Co-authored-by: Lixou <82600264+DasLixou@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
2024-08-15 14:43:55 +00:00
Ben Frankel
9b254aab1e
Explicitly order CameraUpdateSystem before UiSystem::Prepare (#14609)
# Objective

Fixes https://github.com/bevyengine/bevy/issues/14277.

May also fix https://github.com/bevyengine/bevy/issues/14255, needs
verification.

## Solution

Explicitly order `CameraUpdateSystem` before `UiSystem::Prepare`, so
that when the window resizes, `camera_system` will update the `Camera`'s
viewport size before `ui_layout_system` also reacts to the window resize
and tries to read the new `Camera` viewport size to set UI node sizes
accordingly.

## Testing

I tested that explicitly ordering `CameraUpdateSystem` _after_ triggers
the buggy behavior, and explicitly ordering it _before_ does not trigger
the buggy behavior or crash the app (which also demonstrates that the
system sets are ambiguous).

---

## Migration Guide

`CameraUpdateSystem` is now explicitly ordered before
`UiSystem::Prepare` instead of being ambiguous with it.
2024-08-04 13:34:51 +00:00
s-puig
ba09f35474
Fix UI texture atlas with offset (#13620)
# Objective

- Fixes #11219 

## Solution

- Scaling calculations use texture dimensions instead of layout
dimensions.

## Testing

- Did you test these changes? If so, how?

All UI examples look fine.

- How can other people (reviewers) test your changes? Is there anything
specific they need to know?

Example in #11219

## Migration Guide

```diff
let ui_node = ExtractedUiNode {
                    stack_index,
                    transform,
                    color,
                    rect,
                    image,
-                   atlas_size: Some(atlas_size * scale_factor),      
+                   atlas_scaling: Some(Vec2::splat(scale_factor)),
                    clip,
                    flip_x,
                    flip_y,
                    camera_entity,
                    border,
                    border_radius,
                    node_type,
                },
```

```diff
let computed_slices = ComputedTextureSlices {
    slices,
-    image_size,
}
```
2024-07-30 15:31:58 +00:00
Tamás Kiss
396153ae59
fix issue with phantom ui node children (#14490)
# Objective

The `ui_layout_system` relies on change detection to sync parent-child
relation to taffy. The children need to by synced before node removal to
avoid trying to set deleted nodes as children (due to how the different
queries collect entities). This however may leave nodes that were
removed set as children to other nodes in special cases.

Fixes #11385

## Solution

The solution is simply to re-sync the changed children after the nodes
are removed.

## Testing

Tested with `sickle_ui` where docking zone highlights would end up
glitched when docking was done in a certain manner:
- run the `docking_zone_splits` example
- pop out a tab from the top
- dock the floating panel in the center right
- grab another tab and try to hover the original static docking zone:
the highlight is semi-stuck
- (NOTE: sometimes it worked even without the fix due to scheduling
order not producing the bugged query results)

After the fix, the issue is no longer present.

NOTE: The performance impact should be minimal, as the child sync relies
on change detection. The change detection was also the reason the parent
nodes remained "stuck" with the phantom children if no other update were
done to them.
2024-07-29 23:42:56 +00:00
Blake Bedford
eabb58aa04
Add BorderRadius field to ImageBundle (#14457)
# Objective

- Fixes #14453

## Solution

- Added BorderRadius to ImageBundle

## Testing

- Did you test these changes? If so, how?

- Tested on a random picture I found in the examples and it added a
border radius.

- Are there any parts that need more testing?

- I don't fink so.

- How can other people (reviewers) test your changes? Is there anything
specific they need to know?

- Apply a border radius to a random picture.
2024-07-24 18:41:26 +00:00
Sludge
f0ff7fb544
Add and reflect Default impls for CSS grid types (#14443)
# Objective

- Some types here were not constructible via reflection, and some were
missing fairly obvious `Default` values.
- Some types used `#[reflect_value]` for some unstated reason, making
them opaque to reflection-based code.

## Solution

- Add and reflect some `Default` impls, and stop using
`#[reflect_value]`.
2024-07-22 21:39:59 +00:00
IceSentry
3faca1e549
Don't ignore draw errors (#13240)
# Objective

- It's possible to have errors in a draw command, but these errors are
ignored

## Solution

- Return a result with the error

## Changelog

Renamed `RenderCommandResult::Failure` to `RenderCommandResult::Skip`
Added a `reason` string parameter to `RenderCommandResult::Failure`

## Migration Guide
If you were using `RenderCommandResult::Failure` to just ignore an error
and retry later, use `RenderCommandResult::Skip` instead.

This wasn't intentional, but this PR should also help with
https://github.com/bevyengine/bevy/issues/12660 since we can turn a few
unwraps into error messages now.

---------

Co-authored-by: Charlotte McElwain <charlotte.c.mcelwain@gmail.com>
2024-07-22 19:22:30 +00:00
BD103
cd497152bb
Fix error in bevy_ui when building without bevy_text (#14430)
# Objective

- `bevy_ui` does not build without the `bevy_text` feature due to
improper feature gating.
- Specifically, `MeasureArgs<'a>` had an unused lifetime `'a` without
`bevy_text` enabled. This is because it stores a reference to a
`cosmic_text::FontSystem`.
- This was caught by `flag-frenzy` in [this
run](https://github.com/TheBevyFlock/flag-frenzy/actions/runs/10024258523/job/27706132250).

## Solution

- Add a `PhantomData` to `MeasureArgs<'a>` in order to maintain its
lifetime argument.
- I also named it `font_system`, after the feature-gated argument that
actually needs a lifetime, for usability. Please comment if you have a
better solution!
- Move some unused imports to be behind the `bevy_text` feature gate.

## Testing

```bash
# Fails on main.
cargo check -p bevy_ui --no-default-features
# Succeeds on main.
cargo check -p bevy_ui --no-default-features -F bevy_text
```

---

## Migration Guide

**This is not a breaking change for users migrating from 0.14, since
`MeasureArgs` did not exist then.**

When the `bevy_text` feature is disabled for `bevy_ui`, the type of the
`MeasureArgs::font_system` field is now a `PhantomData` instead of being
removed entirely. This is in order to keep the lifetime parameter, even
though it is unused without text being enabled.
2024-07-22 19:19:10 +00:00
Peter Hayman
b8416b3043
Add some missing reflect attributes (#14259)
# Objective

- Some types are missing reflection attributes, which means we can't use
them in scene serialization etc.
- Effected types
   - `BorderRadius`
   - `AnimationTransitions`
   - `OnAdd`
   - `OnInsert`
   - `OnRemove`
- My use-case for `OnAdd` etc to derive reflect is 'Serializable
Observer Components'. Add the component, save the scene, then the
observer is re-added on scene load.

```rust
#[derive(Reflect)]
struct MySerializeableObserver<T: Event>(#[reflect(ignore)]PhantomData<T>);

impl<T: Event> Component for MySerializeableObserver<T> {
  const STORAGE_TYPE: StorageType  = StorageType::Table;
    fn register_component_hooks(hooks: &mut ComponentHooks) {
      hooks.on_add(|mut world, entity, _| {
        world
          .commands()
          .entity(entity)
          .observe(|_trigger: Trigger<T>| {
            println!("it triggered etc.");
          });
    });
  }
}
```

## Solution

- Add the missing traits

---
2024-07-22 18:24:10 +00:00
UkoeHB
c3320627ac
Clean up UiSystem system sets (#14228)
# Objective

- All UI systems should be in system sets that are easy to order around
in user code.

## Solution

- Add `UiSystem::Prepare` and `UiSystem::PostLayout` system sets to
capture floater systems.
- Adjust how UI systems are scheduled to align with the new sets.

This is *mostly* a pure refactor without any behavior/scheduling
changes. See migration guide.

## Testing

- Not tested, correctness by inspection.

---

## Migration Guide

`UiSystem` system set adjustments.
- The `UiSystem::Outline` system set is now strictly ordered after
`UiSystem::Layout`, rather than overlapping it.
2024-07-15 15:27:38 +00:00
Mike
91dd7e6f0f
add entity to error message (#14163)
# Objective

- There was a new warning added about having an unstyled child in the ui
hierarchy. Debugging the new error is pretty hard without any info about
which entity is.

## Solution

- Add the entity id to the warning.

```text
// Before
2024-07-05T19:40:59.904014Z  WARN bevy_ui::layout::ui_surface: Unstyled child in a UI entity hierarchy. You are using an entity without UI components as a child of an entity with UI components, results may be unexpected.

//After
2024-07-05T19:40:59.904014Z  WARN bevy_ui::layout::ui_surface: Unstyled child `3v1` in a UI entity hierarchy. You are using an entity without UI components as a child of an entity with UI components, results may be unexpected.
```

## Changelog

- add entity id to ui surface warning
2024-07-08 01:01:47 +00:00
re0312
db19d2ee47
Optimize ui_layout_system (#14064)
# Objective

- Currently bevy's ui layout system could takes a long time.

## Solution

- cache `default_ui_camera `entity to avoid repetitive lookup


## Performance
cargo run --release --example many_buttons --features bevy/trace_tracy  


![image](https://github.com/bevyengine/bevy/assets/45868716/f8eda0b0-343d-4379-847f-f1636c38e5ec)
2024-07-08 00:48:35 +00:00
TotalKrill
5986d5d309
Cosmic text (#10193)
# Replace ab_glyph with the more capable cosmic-text

Fixes #7616.

Cosmic-text is a more mature text-rendering library that handles scripts
and ligatures better than ab_glyph, it can also handle system fonts
which can be implemented in bevy in the future

Rebase of https://github.com/bevyengine/bevy/pull/8808

## Changelog

Replaces text renderer ab_glyph with cosmic-text

The definition of the font size has changed with the migration to cosmic
text. The behavior is now consistent with other platforms (e.g. the
web), where the font size in pixels measures the height of the font (the
distance between the top of the highest ascender and the bottom of the
lowest descender). Font sizes in your app need to be rescaled to
approximately 1.2x smaller; for example, if you were using a font size
of 60.0, you should now use a font size of 50.0.

## Migration guide

- `Text2dBounds` has been replaced with `TextBounds`, and it now accepts
`Option`s to the bounds, instead of using `f32::INFINITY` to inidicate
lack of bounds
- Textsizes should be changed, dividing the current size with 1.2 will
result in the same size as before.
- `TextSettings` struct is removed
- Feature `subpixel_alignment` has been removed since cosmic-text
already does this automatically
- TextBundles and things rendering texts requires the `CosmicBuffer`
Component on them as well

## Suggested followups:

- TextPipeline: reconstruct byte indices for keeping track of eventual
cursors in text input
- TextPipeline: (future work) split text entities into section entities
- TextPipeline: (future work) text editing
- Support line height as an option. Unitless `1.2` is the default used
in browsers (1.2x font size).
- Support System Fonts and font families
- Example showing of animated text styles. Eg. throbbing hyperlinks

---------

Co-authored-by: tigregalis <anak.harimau@gmail.com>
Co-authored-by: Nico Burns <nico@nicoburns.com>
Co-authored-by: sam edelsten <samedelsten1@gmail.com>
Co-authored-by: Dimchikkk <velo.app1@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Rob Parrett <robparrett@gmail.com>
2024-07-04 20:41:08 +00:00
re0312
1c2f687202
Skip extract UiImage When its texture is default (#14122)
# Objective

- After #14017 , I noticed that the drawcall increased 10x in the
`many_buttons`, causing the `UIPassNode `to increase from 1.5ms to 6ms.
This is because our UI batching is very fragile.

## Solution

- skip extract UiImage when its texture is default


## Performance 
many_buttons UiPassNode

![image](https://github.com/bevyengine/bevy/assets/45868716/9295d958-8c3f-469c-a7e0-d1e90db4dfb7)
2024-07-03 20:54:11 +00:00
re0312
2893fc3e8b
Using simple approx round up in ui_layout_system (#14079)
# Objective

- built-in `f32::round `is slow 
- splits from #14064 
## Solution

- using a simple floor instead of round

## Testing

- I am not an expert on floating-point values, but I enumerated all f32
values to test for potential errors compared to the previous function.
[rust
playground](https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=0d8ed5604499e7bd9c61ce57d47e8c06)

three cases where the behavior differs between the new and previous
functions:
| value  |  previous | new  |  
|---|---|---|
|  [-0.5,0) |  -0 | +0  |   
|  0.49999997  | 0  |  1 |   
| +-8388609 |  8388609   | 8388610  |   

## Performance


![image](https://github.com/bevyengine/bevy/assets/45868716/1910f342-e55b-4f5c-851c-24a142d5c72e)
2024-07-03 12:48:34 +00:00
Lura
856b39d821
Apply Clippy lints regarding lazy evaluation and closures (#14015)
# Objective

- Lazily evaluate
[default](https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_or_default)~~/[or](https://rust-lang.github.io/rust-clippy/master/index.html#/or_fun_call)~~
values where it makes sense
  - ~~`unwrap_or(foo())` -> `unwrap_or_else(|| foo())`~~
  - `unwrap_or(Default::default())` -> `unwrap_or_default()`
  - etc.
- Avoid creating [redundant
closures](https://rust-lang.github.io/rust-clippy/master/index.html#/redundant_closure),
even for [method
calls](https://rust-lang.github.io/rust-clippy/master/index.html#/redundant_closure_for_method_calls)
  - `map(|something| something.into())` -> `map(Into:into)`

## Solution

- Apply Clippy lints:
-
~~[or_fun_call](https://rust-lang.github.io/rust-clippy/master/index.html#/or_fun_call)~~
-
[unwrap_or_default](https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_or_default)
-
[redundant_closure_for_method_calls](https://rust-lang.github.io/rust-clippy/master/index.html#/redundant_closure_for_method_calls)
([redundant
closures](https://rust-lang.github.io/rust-clippy/master/index.html#/redundant_closure)
is already enabled)

## Testing

- Tested on Windows 11 (`stable-x86_64-pc-windows-gnu`, 1.79.0)
- Bevy compiles without errors or warnings and examples seem to work as
intended
  - `cargo clippy` 
  - `cargo run -p ci -- compile` 

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-07-01 15:54:40 +00:00