use crate::state::PseudoState;
use egui::{
Color32, CornerRadius, CursorIcon, FontId, Margin, Rect, Shape, Stroke, Vec2, Visuals, pos2,
style::WidgetVisuals,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BackgroundImageFit {
#[default]
Stretch,
Cover,
}
#[derive(Clone, Copy, Debug)]
pub struct Shadow {
pub offset: Vec2,
pub stroke: Stroke,
pub fill: Option<Color32>,
}
pub fn paint_shadows(
ui: &egui::Ui,
reserve_idx: egui::layers::ShapeIdx,
rect: egui::Rect,
corner_radius: CornerRadius,
shadows: &[Shadow],
) {
if shadows.is_empty() {
return;
}
let shapes: Vec<Shape> = shadows
.iter()
.flat_map(|s| {
let r = rect.translate(s.offset);
let mut parts: Vec<Shape> = Vec::with_capacity(2);
if let Some(fill) = s.fill {
parts.push(Shape::rect_filled(r, corner_radius, fill));
}
if s.stroke != Stroke::NONE {
parts.push(Shape::rect_stroke(
r,
corner_radius,
s.stroke,
egui::StrokeKind::Outside,
));
}
parts
})
.collect();
ui.painter().set(reserve_idx, Shape::Vec(shapes));
}
#[derive(Clone, Copy, Default, Debug)]
pub struct SideStrokes {
pub top: Option<Stroke>,
pub right: Option<Stroke>,
pub bottom: Option<Stroke>,
pub left: Option<Stroke>,
}
impl SideStrokes {
pub fn any(&self) -> bool {
self.top.is_some() || self.right.is_some() || self.bottom.is_some() || self.left.is_some()
}
}
#[derive(Clone, Copy, Debug)]
pub struct ResolvedBorder {
pub top: Stroke,
pub right: Stroke,
pub bottom: Stroke,
pub left: Stroke,
}
pub fn paint_side_borders(painter: &egui::Painter, rect: Rect, border: ResolvedBorder) {
let line = |a, b, stroke: Stroke| {
if stroke.width > 0.0 {
painter.add(Shape::line_segment([a, b], stroke));
}
};
line(rect.left_top(), rect.right_top(), border.top);
line(rect.right_top(), rect.right_bottom(), border.right);
line(rect.left_bottom(), rect.right_bottom(), border.bottom);
line(rect.left_top(), rect.left_bottom(), border.left);
}
fn cover_uv(intrinsic: Vec2, dest: Rect) -> Rect {
if intrinsic.x <= 0.0 || intrinsic.y <= 0.0 {
return Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
}
let dest_aspect = dest.width() / dest.height();
let img_aspect = intrinsic.x / intrinsic.y;
if img_aspect > dest_aspect {
let scale = dest_aspect / img_aspect;
let pad = (1.0 - scale) / 2.0;
Rect::from_min_max(pos2(pad, 0.0), pos2(1.0 - pad, 1.0))
} else {
let scale = img_aspect / dest_aspect;
let pad = (1.0 - scale) / 2.0;
Rect::from_min_max(pos2(0.0, pad), pos2(1.0, 1.0 - pad))
}
}
pub fn justify_body_vertically<R>(
ui: &mut egui::Ui,
fill_height: f32,
justify_factor: f32,
content_h_id: egui::Id,
body: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
let cached_h = ui.ctx().memory(|m| m.data.get_temp::<f32>(content_h_id));
let top_pad = ((fill_height - cached_h.unwrap_or(0.0)) * justify_factor).max(0.0);
ui.add_space(top_pad);
let scope = ui.scope(|ui| {
if cached_h.is_none() {
ui.set_invisible();
}
body(ui)
});
let measured_h = scope.response.rect.height();
ui.ctx()
.memory_mut(|m| m.data.insert_temp(content_h_id, measured_h));
if cached_h.is_none() {
ui.ctx().request_repaint();
}
scope.inner
}
pub fn bgimg_fade_alpha(ctx: &egui::Context, id: egui::Id, duration: f32, ready: bool) -> f32 {
if duration <= 0.0 {
return 1.0;
}
if !ready {
return 0.0;
}
let now = ctx.input(|i| i.time);
let start = ctx.memory_mut(|m| *m.data.get_temp_mut_or_insert_with(id, || now));
let alpha = (((now - start) / duration as f64).clamp(0.0, 1.0)) as f32;
if alpha < 1.0 {
ctx.request_repaint();
}
alpha
}
#[allow(clippy::too_many_arguments)]
pub fn background_image_shape(
ui: &egui::Ui,
rect: Rect,
corner_radius: CornerRadius,
image: &egui::Image<'static>,
fit: BackgroundImageFit,
tint: Color32,
bg: Option<Color32>,
border: Option<Stroke>,
border_sides: Option<ResolvedBorder>,
fade: Option<(egui::Id, f32)>,
) -> Option<Shape> {
use egui::epaint::RectShape;
use egui::load::TexturePoll;
let mut parts: Vec<Shape> = Vec::with_capacity(3);
if let Some(fill) = bg {
parts.push(Shape::rect_filled(rect, corner_radius, fill));
}
let poll = image.load_for_size(ui.ctx(), rect.size()).ok();
if let Some(TexturePoll::Ready { texture }) = poll {
let tint = match fade {
Some((id, duration)) if duration > 0.0 => {
tint.gamma_multiply(bgimg_fade_alpha(ui.ctx(), id, duration, true))
}
_ => tint,
};
let uv = match fit {
BackgroundImageFit::Stretch => Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
BackgroundImageFit::Cover => cover_uv(texture.size, rect),
};
let tex_shape = RectShape::filled(rect, corner_radius, tint).with_texture(texture.id, uv);
parts.push(Shape::Rect(tex_shape));
}
if let Some(sides) = border_sides {
let mut push = |a, b, stroke: Stroke| {
if stroke.width > 0.0 {
parts.push(Shape::line_segment([a, b], stroke));
}
};
push(rect.left_top(), rect.right_top(), sides.top);
push(rect.right_top(), rect.right_bottom(), sides.right);
push(rect.left_bottom(), rect.right_bottom(), sides.bottom);
push(rect.left_top(), rect.left_bottom(), sides.left);
} else if let Some(stroke) = border {
parts.push(Shape::rect_stroke(
rect,
corner_radius,
stroke,
egui::StrokeKind::Outside,
));
}
if parts.is_empty() {
None
} else {
Some(Shape::Vec(parts))
}
}
pub fn render_scoped<R>(ui: &mut egui::Ui, visible: bool, f: impl FnOnce(&mut egui::Ui) -> R) -> R {
ui.scope(|ui| {
if !visible {
ui.set_invisible();
}
f(ui)
})
.inner
}
#[derive(Clone, Default, Debug)]
pub struct SharedStyle {
pub bg: Option<Color32>,
pub hover_bg: Option<Color32>,
pub active_bg: Option<Color32>,
pub focus_bg: Option<Color32>,
pub accent: Option<Color32>,
pub hover_accent: Option<Color32>,
pub text_color: Option<Color32>,
pub hover_text_color: Option<Color32>,
pub font_size: Option<f32>,
pub font_id: Option<FontId>,
pub border: Option<Stroke>,
pub hover_border: Option<Stroke>,
pub focus_border: Option<Stroke>,
pub border_sides: SideStrokes,
pub hover_border_sides: SideStrokes,
pub focus_border_sides: SideStrokes,
pub corner_radius: Option<CornerRadius>,
pub padding: Option<Margin>,
pub margin: Option<Margin>,
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub min_height: Option<f32>,
pub max_height: Option<f32>,
pub full_width: bool,
pub full_height: bool,
pub cursor_icon: Option<CursorIcon>,
pub visible: Option<bool>,
pub shadows: Vec<Shadow>,
pub background_image: Option<egui::Image<'static>>,
pub background_image_fit: BackgroundImageFit,
pub background_image_tint: Option<Color32>,
pub background_image_fade_in: Option<f32>,
pub background_image_fade_content: bool,
}
#[derive(Clone)]
pub struct ResolvedStyle {
pub bg: Color32,
pub text_color: Color32,
pub border: Stroke,
pub border_sides: ResolvedBorder,
pub has_border_overrides: bool,
pub corner_radius: CornerRadius,
pub padding: Margin,
pub margin: Margin,
pub cursor_icon: Option<CursorIcon>,
}
#[derive(Clone)]
pub struct PerStateStyle {
pub inactive: ResolvedStyle,
pub hovered: ResolvedStyle,
pub active: ResolvedStyle,
pub focused: ResolvedStyle,
pub accent: Color32,
pub hover_accent: Color32,
pub corner_radius: CornerRadius,
pub padding: Margin,
pub margin: Margin,
pub cursor_icon: Option<CursorIcon>,
}
impl SharedStyle {
pub fn resolve(&self, state: PseudoState, default: &WidgetVisuals) -> ResolvedStyle {
let bg = match state {
_ if state.active && self.active_bg.is_some() => self.active_bg.unwrap(),
_ if state.hovered && self.hover_bg.is_some() => self.hover_bg.unwrap(),
_ if state.focused && self.focus_bg.is_some() => self.focus_bg.unwrap(),
_ => self.bg.unwrap_or(default.bg_fill),
};
let border = match state {
_ if state.focused && self.focus_border.is_some() => self.focus_border.unwrap(),
_ if state.hovered && self.hover_border.is_some() => self.hover_border.unwrap(),
_ => self.border.unwrap_or(default.bg_stroke),
};
let sides = match state {
_ if state.focused && self.focus_border_sides.any() => self.focus_border_sides,
_ if state.hovered && self.hover_border_sides.any() => self.hover_border_sides,
_ => self.border_sides,
};
let has_border_overrides = sides.any();
let border_sides = ResolvedBorder {
top: sides.top.unwrap_or(border),
right: sides.right.unwrap_or(border),
bottom: sides.bottom.unwrap_or(border),
left: sides.left.unwrap_or(border),
};
let text_color = match state {
_ if state.hovered && self.hover_text_color.is_some() => self.hover_text_color.unwrap(),
_ => self.text_color.unwrap_or(default.text_color()),
};
ResolvedStyle {
bg,
text_color,
border,
border_sides,
has_border_overrides,
corner_radius: self.corner_radius.unwrap_or(default.corner_radius),
padding: self.padding.unwrap_or_default(),
margin: self.margin.unwrap_or_default(),
cursor_icon: self.cursor_icon,
}
}
pub fn resolve_per_state(&self, visuals: &Visuals) -> PerStateStyle {
let resolve_one = |state: PseudoState, wv: &WidgetVisuals| self.resolve(state, wv);
let inactive = resolve_one(
PseudoState {
hovered: false,
active: false,
focused: false,
},
&visuals.widgets.inactive,
);
let hovered = resolve_one(
PseudoState {
hovered: true,
active: false,
focused: false,
},
&visuals.widgets.hovered,
);
let active = resolve_one(
PseudoState {
hovered: true,
active: true,
focused: false,
},
&visuals.widgets.active,
);
let focused = resolve_one(
PseudoState {
hovered: false,
active: false,
focused: true,
},
&visuals.widgets.inactive,
);
let corner_radius = self
.corner_radius
.unwrap_or(visuals.widgets.inactive.corner_radius);
let accent = self.accent.unwrap_or(visuals.selection.bg_fill);
let hover_accent = self.hover_accent.unwrap_or(accent);
PerStateStyle {
inactive,
hovered,
active,
focused,
accent,
hover_accent,
corner_radius,
padding: self.padding.unwrap_or_default(),
margin: self.margin.unwrap_or_default(),
cursor_icon: self.cursor_icon,
}
}
pub fn apply_to_visuals(per: &PerStateStyle, vis: &mut Visuals) {
let states: [(&ResolvedStyle, &mut WidgetVisuals); 3] = [
(&per.inactive, &mut vis.widgets.inactive),
(&per.hovered, &mut vis.widgets.hovered),
(&per.active, &mut vis.widgets.active),
];
for (resolved, wv) in states {
wv.bg_fill = resolved.bg;
wv.weak_bg_fill = resolved.bg;
wv.bg_stroke = if resolved.has_border_overrides {
Stroke::NONE
} else {
resolved.border
};
wv.corner_radius = per.corner_radius;
wv.expansion = 0.0;
wv.fg_stroke = Stroke::new(wv.fg_stroke.width, resolved.text_color);
}
vis.widgets.open.bg_fill = per.inactive.bg;
vis.widgets.open.weak_bg_fill = per.inactive.bg;
vis.widgets.open.bg_stroke = if per.inactive.has_border_overrides {
Stroke::NONE
} else {
per.inactive.border
};
vis.widgets.open.corner_radius = per.corner_radius;
vis.widgets.open.expansion = 0.0;
vis.extreme_bg_color = per.inactive.bg;
vis.selection.bg_fill = per.accent;
vis.selection.stroke = per.focused.border;
}
pub fn for_response<'a>(
per: &'a PerStateStyle,
response: &egui::Response,
) -> &'a ResolvedStyle {
if response.has_focus() {
&per.focused
} else if response.is_pointer_button_down_on() {
&per.active
} else if response.hovered() {
&per.hovered
} else {
&per.inactive
}
}
pub fn paint_widget_side_borders(
ui: &egui::Ui,
response: &egui::Response,
per: &PerStateStyle,
) {
let state = Self::for_response(per, response);
if state.has_border_overrides {
paint_side_borders(ui.painter(), response.rect, state.border_sides);
}
}
pub fn has_frame_styles(&self) -> bool {
self.bg.is_some()
|| self.hover_bg.is_some()
|| self.active_bg.is_some()
|| self.focus_bg.is_some()
|| self.border.is_some()
|| self.hover_border.is_some()
|| self.focus_border.is_some()
|| self.border_sides.any()
|| self.hover_border_sides.any()
|| self.focus_border_sides.any()
|| self.padding.is_some()
|| self.corner_radius.is_some()
|| self.margin.is_some()
|| self.background_image.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::Visuals;
fn default_visuals() -> WidgetVisuals {
Visuals::default().widgets.inactive
}
fn style_with_all_bgs() -> SharedStyle {
SharedStyle {
bg: Some(Color32::RED),
hover_bg: Some(Color32::GREEN),
active_bg: Some(Color32::BLUE),
focus_bg: Some(Color32::YELLOW),
..Default::default()
}
}
#[test]
fn bg_active_beats_hovered_and_focused() {
let style = style_with_all_bgs();
let state = PseudoState {
hovered: true,
active: true,
focused: true,
};
let resolved = style.resolve(state, &default_visuals());
assert_eq!(resolved.bg, Color32::BLUE);
}
#[test]
fn bg_hovered_beats_focused() {
let style = style_with_all_bgs();
let state = PseudoState {
hovered: true,
active: false,
focused: true,
};
let resolved = style.resolve(state, &default_visuals());
assert_eq!(resolved.bg, Color32::GREEN);
}
#[test]
fn bg_focused_used_when_only_focused() {
let style = style_with_all_bgs();
let state = PseudoState {
hovered: false,
active: false,
focused: true,
};
let resolved = style.resolve(state, &default_visuals());
assert_eq!(resolved.bg, Color32::YELLOW);
}
#[test]
fn bg_base_used_when_no_pseudo_state() {
let style = style_with_all_bgs();
let resolved = style.resolve(PseudoState::default(), &default_visuals());
assert_eq!(resolved.bg, Color32::RED);
}
#[test]
fn falls_back_to_default_visuals_when_unset() {
let style = SharedStyle::default();
let visuals = default_visuals();
let resolved = style.resolve(PseudoState::default(), &visuals);
assert_eq!(resolved.bg, visuals.bg_fill);
assert_eq!(resolved.border, visuals.bg_stroke);
assert_eq!(resolved.corner_radius, visuals.corner_radius);
assert_eq!(resolved.text_color, visuals.text_color());
}
#[test]
fn hover_text_color_only_applies_when_hovered() {
let style = SharedStyle {
text_color: Some(Color32::RED),
hover_text_color: Some(Color32::GREEN),
..Default::default()
};
let visuals = default_visuals();
let active = PseudoState {
hovered: false,
active: true,
focused: false,
};
assert_eq!(style.resolve(active, &visuals).text_color, Color32::RED);
let focused = PseudoState {
hovered: false,
active: false,
focused: true,
};
assert_eq!(style.resolve(focused, &visuals).text_color, Color32::RED);
let hovered = PseudoState {
hovered: true,
active: false,
focused: false,
};
assert_eq!(style.resolve(hovered, &visuals).text_color, Color32::GREEN);
}
#[test]
fn border_focused_beats_hovered() {
let hover = Stroke::new(1.0, Color32::GREEN);
let focus = Stroke::new(2.0, Color32::YELLOW);
let style = SharedStyle {
hover_border: Some(hover),
focus_border: Some(focus),
..Default::default()
};
let state = PseudoState {
hovered: true,
active: false,
focused: true,
};
let resolved = style.resolve(state, &default_visuals());
assert_eq!(resolved.border, focus);
}
#[test]
fn side_override_sets_only_that_side() {
let left = Stroke::new(3.0, Color32::RED);
let style = SharedStyle {
border_sides: SideStrokes {
left: Some(left),
..Default::default()
},
..Default::default()
};
let visuals = default_visuals();
let resolved = style.resolve(PseudoState::default(), &visuals);
assert!(resolved.has_border_overrides);
assert_eq!(resolved.border_sides.left, left);
assert_eq!(resolved.border_sides.top, visuals.bg_stroke);
assert_eq!(resolved.border_sides.right, visuals.bg_stroke);
assert_eq!(resolved.border_sides.bottom, visuals.bg_stroke);
}
#[test]
fn side_override_falls_back_to_uniform_border() {
let uniform = Stroke::new(1.0, Color32::GRAY);
let left = Stroke::new(3.0, Color32::RED);
let style = SharedStyle {
border: Some(uniform),
border_sides: SideStrokes {
left: Some(left),
..Default::default()
},
..Default::default()
};
let resolved = style.resolve(PseudoState::default(), &default_visuals());
assert_eq!(resolved.border_sides.left, left);
assert_eq!(resolved.border_sides.top, uniform);
assert_eq!(resolved.border_sides.right, uniform);
assert_eq!(resolved.border_sides.bottom, uniform);
assert_eq!(resolved.border, uniform);
}
#[test]
fn no_side_override_means_no_overrides_flag() {
let uniform = Stroke::new(1.0, Color32::GRAY);
let style = SharedStyle {
border: Some(uniform),
..Default::default()
};
let resolved = style.resolve(PseudoState::default(), &default_visuals());
assert!(!resolved.has_border_overrides);
assert_eq!(resolved.border, uniform);
}
#[test]
fn focus_side_override_beats_base() {
let base_left = Stroke::new(1.0, Color32::GRAY);
let focus_left = Stroke::new(2.0, Color32::YELLOW);
let style = SharedStyle {
border_sides: SideStrokes {
left: Some(base_left),
..Default::default()
},
focus_border_sides: SideStrokes {
left: Some(focus_left),
..Default::default()
},
..Default::default()
};
let focused = PseudoState {
hovered: false,
active: false,
focused: true,
};
assert_eq!(
style.resolve(focused, &default_visuals()).border_sides.left,
focus_left
);
assert_eq!(
style
.resolve(PseudoState::default(), &default_visuals())
.border_sides
.left,
base_left
);
}
#[test]
fn paint_side_borders_only_emits_positive_width_sides() {
let ctx = egui::Context::default();
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
let rect = Rect::from_min_size(pos2(0.0, 0.0), Vec2::new(50.0, 30.0));
let border = ResolvedBorder {
top: Stroke::NONE,
right: Stroke::new(2.0, Color32::RED),
bottom: Stroke::new(0.0, Color32::RED),
left: Stroke::new(2.0, Color32::RED),
};
paint_side_borders(ui.painter(), rect, border);
});
let segments = output
.shapes
.iter()
.filter(|cs| matches!(cs.shape, Shape::LineSegment { .. }))
.count();
assert_eq!(segments, 2, "expected one segment per positive-width side");
}
#[test]
fn has_frame_styles_empty() {
assert!(!SharedStyle::default().has_frame_styles());
}
#[test]
fn has_frame_styles_each_trigger() {
let triggers: Vec<(&str, SharedStyle)> = vec![
(
"bg",
SharedStyle {
bg: Some(Color32::RED),
..Default::default()
},
),
(
"hover_bg",
SharedStyle {
hover_bg: Some(Color32::RED),
..Default::default()
},
),
(
"active_bg",
SharedStyle {
active_bg: Some(Color32::RED),
..Default::default()
},
),
(
"focus_bg",
SharedStyle {
focus_bg: Some(Color32::RED),
..Default::default()
},
),
(
"border",
SharedStyle {
border: Some(Stroke::new(1.0, Color32::RED)),
..Default::default()
},
),
(
"hover_border",
SharedStyle {
hover_border: Some(Stroke::new(1.0, Color32::RED)),
..Default::default()
},
),
(
"focus_border",
SharedStyle {
focus_border: Some(Stroke::new(1.0, Color32::RED)),
..Default::default()
},
),
(
"padding",
SharedStyle {
padding: Some(egui::Margin::same(4)),
..Default::default()
},
),
(
"corner_radius",
SharedStyle {
corner_radius: Some(egui::CornerRadius::same(4)),
..Default::default()
},
),
(
"margin",
SharedStyle {
margin: Some(egui::Margin::same(4)),
..Default::default()
},
),
(
"border_sides",
SharedStyle {
border_sides: SideStrokes {
left: Some(Stroke::new(1.0, Color32::RED)),
..Default::default()
},
..Default::default()
},
),
];
for (name, style) in triggers {
assert!(
style.has_frame_styles(),
"{name} did not trigger has_frame_styles"
);
}
}
#[test]
fn has_frame_styles_ignores_non_frame_props() {
let style = SharedStyle {
text_color: Some(Color32::RED),
full_width: true,
..Default::default()
};
assert!(!style.has_frame_styles());
}
#[test]
fn resolve_per_state_each_variant_independent() {
let style = SharedStyle {
bg: Some(Color32::RED),
hover_bg: Some(Color32::GREEN),
active_bg: Some(Color32::BLUE),
focus_bg: Some(Color32::YELLOW),
accent: Some(Color32::WHITE),
..Default::default()
};
let vis = Visuals::default();
let per = style.resolve_per_state(&vis);
assert_eq!(per.inactive.bg, Color32::RED);
assert_eq!(per.hovered.bg, Color32::GREEN);
assert_eq!(per.active.bg, Color32::BLUE);
assert_eq!(per.focused.bg, Color32::YELLOW);
assert_eq!(per.accent, Color32::WHITE);
}
#[test]
fn resolve_per_state_accent_falls_back_to_selection_bg_fill() {
let style = SharedStyle::default();
let vis = Visuals::default();
let per = style.resolve_per_state(&vis);
assert_eq!(per.accent, vis.selection.bg_fill);
}
#[test]
fn background_image_fit_default_is_stretch() {
assert_eq!(BackgroundImageFit::default(), BackgroundImageFit::Stretch);
}
#[test]
fn has_frame_styles_triggered_by_background_image() {
let style = SharedStyle {
background_image: Some(egui::Image::from_bytes("bytes://test", vec![])),
..Default::default()
};
assert!(style.has_frame_styles());
}
#[test]
fn cover_uv_stretch_returns_full() {
let dest = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::new(100.0, 100.0));
let uv = cover_uv(egui::Vec2::new(200.0, 100.0), dest);
let uv_cover = cover_uv(egui::Vec2::new(200.0, 100.0), dest);
assert!((uv_cover.min.x - 0.25).abs() < 1e-5);
assert!((uv_cover.max.x - 0.75).abs() < 1e-5);
assert!((uv_cover.min.y - 0.0).abs() < 1e-5);
assert!((uv_cover.max.y - 1.0).abs() < 1e-5);
let _ = uv;
}
#[test]
fn cover_uv_tall_image_crops_vertically() {
let dest = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::new(100.0, 100.0));
let uv = cover_uv(egui::Vec2::new(100.0, 200.0), dest);
assert!((uv.min.x - 0.0).abs() < 1e-5);
assert!((uv.max.x - 1.0).abs() < 1e-5);
assert!((uv.min.y - 0.25).abs() < 1e-5);
assert!((uv.max.y - 0.75).abs() < 1e-5);
}
#[test]
fn cover_uv_square_image_returns_full() {
let dest = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::new(100.0, 100.0));
let uv = cover_uv(egui::Vec2::new(100.0, 100.0), dest);
assert!((uv.min.x - 0.0).abs() < 1e-5);
assert!((uv.max.x - 1.0).abs() < 1e-5);
assert!((uv.min.y - 0.0).abs() < 1e-5);
assert!((uv.max.y - 1.0).abs() < 1e-5);
}
}