use egui::{Align, Color32, InnerResponse, Layout, Shape, Ui};
use crate::{
impl_style_builders,
style::shared_style::{SharedStyle, background_image_shape, paint_shadows},
};
pub struct StyledFrame {
pub style: SharedStyle,
pub align: Option<Align>,
pub justify: Option<Align>,
pub gap: Option<f32>,
pub fill_size: Option<egui::Vec2>,
}
impl Default for StyledFrame {
fn default() -> Self {
Self::new()
}
}
impl StyledFrame {
pub fn new() -> Self {
Self {
style: SharedStyle::default(),
align: None,
justify: None,
gap: None,
fill_size: None,
}
}
pub fn align(mut self, align: Align) -> Self {
self.align = Some(align);
self
}
pub fn justify(mut self, justify: Align) -> Self {
self.justify = Some(justify);
self
}
pub fn gap(mut self, gap: f32) -> Self {
self.gap = Some(gap);
self
}
pub fn show<R>(self, ui: &mut Ui, body: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
if self.style.visible == Some(false) {
ui.set_invisible();
}
let shadow_idx = ui.painter().add(Shape::Noop);
let corner_radius = self.style.corner_radius.unwrap_or_default();
let shadows = self.style.shadows.clone();
let has_bg_image = self.style.background_image.is_some();
let fade_id = has_bg_image
.then(|| ui.make_persistent_id(ui.next_auto_id()).with("__bgimg_fade_start"));
let mut frame = egui::Frame::default();
if has_bg_image {
if let Some(r) = self.style.corner_radius {
frame = frame.corner_radius(r);
}
if let Some(p) = self.style.padding {
frame = frame.inner_margin(p);
}
if let Some(m) = self.style.margin {
frame = frame.outer_margin(m);
}
} else {
if let Some(bg) = self.style.bg {
frame = frame.fill(bg);
}
if let Some(r) = self.style.corner_radius {
frame = frame.corner_radius(r);
}
if let Some(p) = self.style.padding {
frame = frame.inner_margin(p);
}
if let Some(m) = self.style.margin {
frame = frame.outer_margin(m);
}
if let Some(b) = self.style.border {
frame = frame.stroke(b);
}
}
let full_width = self.style.full_width;
let align = self.align;
let justify = self.justify;
let gap = self.gap;
let fill_size = self.fill_size;
let bgimg_slot = has_bg_image.then(|| ui.painter().add(Shape::Noop));
let response = frame.show(ui, |ui| {
if let Some(size) = fill_size {
ui.set_min_size(size);
ui.set_max_size(size);
}
if full_width {
ui.set_min_width(ui.available_width());
}
if let Some(g) = gap {
ui.spacing_mut().item_spacing = egui::Vec2::splat(g);
}
if align.is_some() || justify.is_some() {
let mut layout = Layout::top_down(align.unwrap_or(Align::Min));
if let Some(j) = justify {
layout = layout.with_main_align(j);
}
ui.with_layout(layout, body).inner
} else {
body(ui)
}
});
if let (Some(slot), Some(image)) = (bgimg_slot, self.style.background_image) {
let rect = response.response.rect;
let tint = self.style.background_image_tint.unwrap_or(Color32::WHITE);
let fit = self.style.background_image_fit;
let fade = self.style.background_image_fade_in
.zip(fade_id)
.map(|(d, id)| (id, d));
if let Some(shape) = background_image_shape(
ui,
rect,
corner_radius,
&image,
fit,
tint,
self.style.bg,
self.style.border,
fade,
) {
ui.painter().set(slot, shape);
}
}
paint_shadows(
ui,
shadow_idx,
response.response.rect,
corner_radius,
&shadows,
);
response
}
}
impl_style_builders!(StyledFrame);
#[cfg(test)]
mod tests {
use super::*;
use egui::{Color32, epaint::Shape};
fn load_ready_texture(ctx: &egui::Context) -> egui::Image<'static> {
let handle = ctx.load_texture(
"fade_test",
egui::ColorImage::new([4, 4], vec![Color32::WHITE; 16]),
Default::default(),
);
let sized = egui::load::SizedTexture::from_handle(&handle);
egui::Image::from_texture(sized)
}
fn textured_rect_alpha(shapes: &[Shape]) -> Option<u8> {
for shape in shapes {
match shape {
Shape::Vec(inner) => {
if let Some(a) = textured_rect_alpha(inner) {
return Some(a);
}
}
Shape::Rect(rs) if rs.brush.is_some() => {
return Some(rs.fill.a());
}
_ => {}
}
}
None
}
fn run_frame_at(
ctx: &egui::Context,
img: egui::Image<'static>,
fade_secs: Option<f32>,
time: f64,
) -> Vec<Shape> {
let mut raw = egui::RawInput::default();
raw.time = Some(time);
let output = ctx.run_ui(raw, |ui| {
let mut frame = StyledFrame::new().background_image(img.clone());
if let Some(d) = fade_secs {
frame = frame.background_image_fade_in(d);
}
frame.show(ui, |_ui| {});
});
output.shapes.into_iter().map(|cs| cs.shape).collect()
}
#[test]
fn fade_in_alpha_increases_and_reaches_full() {
let ctx = egui::Context::default();
let img = load_ready_texture(&ctx);
let shapes0 = run_frame_at(&ctx, img.clone(), Some(0.5), 0.0);
let alpha0 = textured_rect_alpha(&shapes0).expect("textured rect present");
let shapes1 = run_frame_at(&ctx, img.clone(), Some(0.5), 0.25);
let alpha1 = textured_rect_alpha(&shapes1).expect("textured rect present");
let shapes2 = run_frame_at(&ctx, img.clone(), Some(0.5), 0.6);
let alpha2 = textured_rect_alpha(&shapes2).expect("textured rect present");
assert!(alpha0 < alpha1, "alpha should increase: {alpha0} < {alpha1}");
assert_eq!(alpha2, 255, "alpha should be full at >= duration");
}
#[test]
fn no_fade_paints_full_opacity_on_first_ready_frame() {
let ctx = egui::Context::default();
let img = load_ready_texture(&ctx);
let shapes = run_frame_at(&ctx, img, None, 0.0);
let alpha = textured_rect_alpha(&shapes).expect("textured rect present");
assert_eq!(alpha, 255, "no-fade path must paint at full opacity");
}
}