1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! Icons: a small image the skin sizes and tints, for the host's own art.
use bevy::asset::LoadState;
use bevy::prelude::*;
/// The picture an [`Icon`] draws: a whole image, or one cell of a texture atlas the host laid out
/// (a sprite sheet of white-on-transparent glyphs is the usual one: the skin tints it).
#[derive(Clone, Debug, PartialEq, Reflect)]
pub enum IconSource {
/// The whole image.
Image(Handle<Image>),
/// One cell of an atlas.
Atlas {
/// The atlas's image.
image: Handle<Image>,
/// How the image is cut up.
layout: Handle<TextureAtlasLayout>,
/// Which cell.
index: usize,
},
}
/// What an [`icon`](crate::icon) shows. Change it and the picture changes in the same frame
/// ([`update_icons`]). The size and the tint are the skin's: see [`IconSize`] and [`IconTone`].
#[derive(Component, Clone, Debug, PartialEq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(IconTone, IconSize, ImageNode)]
pub struct Icon(pub IconSource);
/// Which colour of the skin tints an icon. The image is multiplied by it, so draw icons white on
/// transparent and let the skin colour them; a tone is a colour of the skin and never a number in
/// the host.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum IconTone {
/// The text colour, following the widget the icon is in as its text does: muted while that
/// widget is disabled, the accent's text while it is pressed.
#[default]
Text,
/// [`crate::GooeySkin::text_muted`], for an icon that is a hint.
Muted,
/// [`crate::GooeySkin::accent`], for an icon that says "on" or "selected".
Accent,
/// [`crate::GooeySkin::danger`], for a destructive command.
Danger,
}
/// An icon's side: a step of [`crate::GooeySkin::type_scale`], so it is as tall as the text of the
/// same step and grows with the player's text size. The default, step 2, is the body's line.
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
pub struct IconSize(pub usize);
impl Default for IconSize {
fn default() -> Self {
Self(2)
}
}
/// On an [`Icon`] whose image failed to load, or that has none. The icon keeps its size and draws
/// nothing, so a toolbar never reflows; a warning is logged once when the marker is added.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
pub struct IconMissing;
/// Puts each changed [`Icon`]'s picture on its [`ImageNode`], and marks an icon whose image is
/// missing ([`IconMissing`], one warning). Runs in [`crate::GooeySystems::Style`] before
/// [`crate::apply_skin`].
pub fn update_icons(
mut commands: Commands,
assets: Option<Res<Assets<Image>>>,
server: Option<Res<AssetServer>>,
mut icons: Query<(Entity, Ref<Icon>, &mut ImageNode, Has<IconMissing>)>,
) {
for (entity, icon, mut image, mut missing) in &mut icons {
if icon.is_changed() {
let (handle, atlas) = match &icon.0 {
IconSource::Image(handle) => (handle, None),
IconSource::Atlas {
image,
layout,
index,
} => (
image,
Some(TextureAtlas {
layout: layout.clone(),
index: *index,
}),
),
};
if image.image != *handle {
image.image = handle.clone();
}
if image.texture_atlas != atlas {
image.texture_atlas = atlas;
}
if missing {
commands.entity(entity).remove::<IconMissing>();
missing = false;
}
} else if missing {
continue;
}
let source = match &icon.0 {
IconSource::Image(handle) | IconSource::Atlas { image: handle, .. } => handle,
};
if assets
.as_ref()
.is_some_and(|assets| assets.contains(source))
{
continue;
}
// Not there yet is not missing: a handle the server is still loading says `Loading`.
let gone = server.as_ref().is_none_or(|server| {
matches!(
server.load_state(source),
LoadState::Failed(_) | LoadState::NotLoaded
)
});
if gone && !missing {
warn!("bevy_gooey: icon {entity}: its image is missing; drawing an empty box");
commands.entity(entity).insert(IconMissing);
}
}
}