use egui::{Color32, Id, Image, Response, Shape, Ui, Vec2};
use crate::{
impl_style_builders,
state::PseudoState,
style::shared_style::{
SharedStyle, border_gradient_mesh, gradient_shape, inner_glow_shape, paint_shadows,
paint_side_borders, render_scoped,
},
};
pub struct StyledImage {
image: Image<'static>,
tint: Option<Color32>,
hover_tint: Option<Color32>,
fit: ImageFitSpec,
id_override: Option<Id>,
style: SharedStyle,
}
#[derive(Clone, Copy, Debug, Default)]
enum ImageFitSpec {
#[default]
Default,
ExactSize(Vec2),
MaxSize(Vec2),
FitToFraction(Vec2),
FitToOriginal(f32),
}
impl StyledImage {
pub fn new(image: impl Into<Image<'static>>) -> Self {
Self {
image: image.into(),
tint: None,
hover_tint: None,
fit: ImageFitSpec::Default,
id_override: None,
style: SharedStyle::default(),
}
}
pub fn tint(mut self, tint: Color32) -> Self {
self.tint = Some(tint);
self
}
pub fn hover_tint(mut self, tint: Color32) -> Self {
self.hover_tint = Some(tint);
self
}
pub fn size(mut self, size: Vec2) -> Self {
self.fit = ImageFitSpec::ExactSize(size);
self
}
pub fn max_size(mut self, size: Vec2) -> Self {
self.fit = ImageFitSpec::MaxSize(size);
self
}
pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
self.fit = ImageFitSpec::FitToFraction(fraction);
self
}
pub fn fit_to_original(mut self, scale: f32) -> Self {
self.fit = ImageFitSpec::FitToOriginal(scale);
self
}
pub fn id(mut self, id: impl std::hash::Hash) -> Self {
self.id_override = Some(Id::new(id));
self
}
pub fn show(self, ui: &mut Ui) -> Response {
let visible = self.style.visible != Some(false);
let id = self
.id_override
.unwrap_or_else(|| ui.make_persistent_id(ui.next_auto_id()));
let pseudo = PseudoState::load(ui, id);
let per = self.style.resolve_per_state(ui.visuals());
let response = render_scoped(ui, visible, |ui| {
let shadow_idx = ui.painter().add(Shape::Noop);
let gradient_idx = ui.painter().add(Shape::Noop);
let current_tint = if pseudo.hovered {
self.hover_tint.or(self.tint).unwrap_or(Color32::WHITE)
} else {
self.tint.unwrap_or(Color32::WHITE)
};
let mut img = self
.image
.corner_radius(per.corner_radius)
.tint(current_tint);
match self.fit {
ImageFitSpec::Default => {}
ImageFitSpec::ExactSize(s) => img = img.fit_to_exact_size(s),
ImageFitSpec::MaxSize(s) => img = img.max_size(s),
ImageFitSpec::FitToFraction(f) => img = img.fit_to_fraction(f),
ImageFitSpec::FitToOriginal(s) => img = img.fit_to_original_size(s),
}
let mut wrapper = egui::Frame::new();
if per.margin != egui::Margin::ZERO {
wrapper = wrapper.outer_margin(per.margin);
}
let resp = wrapper
.show(ui, |ui| {
self.style
.resolve_size(ui.available_width(), ui.available_height())
.apply_to_ui(ui);
let resp = ui.add(img);
let state = if pseudo.hovered {
&per.hovered
} else {
&per.inactive
};
if let Some(g) = &state.bg_gradient {
ui.painter().set(
gradient_idx,
gradient_shape(ui.ctx(), resp.rect, per.corner_radius, g),
);
}
if let Some(bg) = state.border_gradient {
ui.painter()
.add(Shape::Mesh(border_gradient_mesh(resp.rect, bg).into()));
} else if state.has_border_overrides {
paint_side_borders(ui.painter(), resp.rect, state.border_sides);
} else if state.border != egui::Stroke::NONE {
ui.painter().rect_stroke(
resp.rect,
per.corner_radius,
state.border,
egui::StrokeKind::Outside,
);
}
if let Some(glow) = state.inner_glow
&& let Some(shape) = inner_glow_shape(resp.rect, per.corner_radius, glow)
{
ui.painter().add(shape);
}
resp
})
.inner;
paint_shadows(
ui,
shadow_idx,
resp.rect,
per.corner_radius,
&self.style.shadows,
);
resp
});
PseudoState::from_response(&response).store(ui, id);
if let Some(icon) = per.cursor_icon
&& response.hovered()
{
ui.ctx().set_cursor_icon(icon);
}
let _ = pseudo;
response
}
}
impl_style_builders!(StyledImage);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_tint_is_none() {
let img = StyledImage::new(egui::Image::from_bytes("bytes://test", vec![]));
assert!(img.tint.is_none());
assert!(img.hover_tint.is_none());
}
#[test]
fn tint_builder_sets_field() {
let img =
StyledImage::new(egui::Image::from_bytes("bytes://test", vec![])).tint(Color32::RED);
assert_eq!(img.tint, Some(Color32::RED));
}
#[test]
fn hover_tint_builder_sets_field() {
let img = StyledImage::new(egui::Image::from_bytes("bytes://test", vec![]))
.hover_tint(Color32::BLUE);
assert_eq!(img.hover_tint, Some(Color32::BLUE));
}
#[test]
fn size_builder_sets_exact_fit() {
let size = Vec2::new(64.0, 64.0);
let img = StyledImage::new(egui::Image::from_bytes("bytes://test", vec![])).size(size);
assert!(matches!(img.fit, ImageFitSpec::ExactSize(s) if s == size));
}
#[test]
fn max_size_builder_sets_max_fit() {
let size = Vec2::new(128.0, 128.0);
let img = StyledImage::new(egui::Image::from_bytes("bytes://test", vec![])).max_size(size);
assert!(matches!(img.fit, ImageFitSpec::MaxSize(s) if s == size));
}
#[test]
fn visible_false_captured_in_style() {
let img = StyledImage::new(egui::Image::from_bytes("bytes://test", vec![])).visible(false);
assert_eq!(img.style.visible, Some(false));
}
#[test]
fn id_override_sets_field() {
let img = StyledImage::new(egui::Image::from_bytes("bytes://test", vec![])).id("my_image");
assert!(img.id_override.is_some());
}
}