use crate::state::PseudoState;
use egui::{
Color32, CornerRadius, CursorIcon, FontId, Margin, Mesh, 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(crate) 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, Debug, PartialEq, Eq, Hash)]
pub struct BgGradient {
pub top_left: Color32,
pub top_right: Color32,
pub bottom_left: Color32,
pub bottom_right: Color32,
}
impl BgGradient {
pub fn corners(tl: Color32, tr: Color32, bl: Color32, br: Color32) -> Self {
Self {
top_left: tl,
top_right: tr,
bottom_left: bl,
bottom_right: br,
}
}
pub fn vertical(top: Color32, bottom: Color32) -> Self {
Self {
top_left: top,
top_right: top,
bottom_left: bottom,
bottom_right: bottom,
}
}
pub fn horizontal(left: Color32, right: Color32) -> Self {
Self {
top_left: left,
top_right: right,
bottom_left: left,
bottom_right: right,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GradientAxis {
Vertical,
Horizontal,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearGradient {
pub stops: Vec<(f32, Color32)>,
pub axis: GradientAxis,
}
impl LinearGradient {
pub fn new(stops: impl IntoIterator<Item = (f32, Color32)>, axis: GradientAxis) -> Self {
let mut stops: Vec<(f32, Color32)> = stops.into_iter().collect();
stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
Self { stops, axis }
}
pub fn evenly_spaced(colors: impl IntoIterator<Item = Color32>, axis: GradientAxis) -> Self {
let colors: Vec<Color32> = colors.into_iter().collect();
let n = colors.len();
let stops = colors.into_iter().enumerate().map(|(i, c)| {
let pos = if n <= 1 {
0.0
} else {
i as f32 / (n - 1) as f32
};
(pos, c)
});
Self::new(stops, axis)
}
pub fn sample(&self, t: f32) -> Color32 {
match self.stops.as_slice() {
[] => Color32::TRANSPARENT,
[(_, c)] => *c,
stops => {
let t = t.clamp(0.0, 1.0);
if t <= stops[0].0 {
return stops[0].1;
}
if t >= stops[stops.len() - 1].0 {
return stops[stops.len() - 1].1;
}
let hi = stops.iter().position(|s| s.0 >= t).unwrap();
let (p0, c0) = stops[hi - 1];
let (p1, c1) = stops[hi];
let span = (p1 - p0).max(f32::EPSILON);
c0.lerp_to_gamma(c1, (t - p0) / span)
}
}
}
}
impl std::hash::Hash for LinearGradient {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.axis.hash(state);
self.stops.len().hash(state);
for (pos, color) in &self.stops {
pos.to_bits().hash(state);
color.hash(state);
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Gradient {
Corners(BgGradient),
Linear(LinearGradient),
}
impl std::hash::Hash for Gradient {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Gradient::Corners(g) => {
0u8.hash(state);
g.hash(state);
}
Gradient::Linear(g) => {
1u8.hash(state);
g.hash(state);
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InnerGlow {
pub width: f32,
pub color: Color32,
pub sides: Sides,
}
impl InnerGlow {
pub fn new(width: f32, color: Color32) -> Self {
Self {
width,
color,
sides: Sides::ALL,
}
}
pub fn with_sides(width: f32, color: Color32, sides: Sides) -> Self {
Self {
width,
color,
sides,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Sides {
pub top: bool,
pub right: bool,
pub bottom: bool,
pub left: bool,
}
impl Sides {
pub const ALL: Sides = Sides {
top: true,
right: true,
bottom: true,
left: true,
};
pub const Y: Sides = Sides {
top: true,
right: false,
bottom: true,
left: false,
};
pub const X: Sides = Sides {
top: false,
right: true,
bottom: false,
left: true,
};
pub const TOP: Sides = Sides {
top: true,
right: false,
bottom: false,
left: false,
};
pub const RIGHT: Sides = Sides {
top: false,
right: true,
bottom: false,
left: false,
};
pub const BOTTOM: Sides = Sides {
top: false,
right: false,
bottom: true,
left: false,
};
pub const LEFT: Sides = Sides {
top: false,
right: false,
bottom: false,
left: true,
};
pub fn is_all(&self) -> bool {
self.top && self.right && self.bottom && self.left
}
pub fn any(&self) -> bool {
self.top || self.right || self.bottom || self.left
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BorderGradient {
pub width: f32,
pub top: Color32,
pub bottom: Color32,
}
impl BorderGradient {
pub fn new(width: f32, top: Color32, bottom: Color32) -> Self {
Self { width, top, bottom }
}
}
#[derive(Clone, Copy, Default, Debug)]
pub(crate) 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(crate) struct ResolvedBorder {
pub top: Stroke,
pub right: Stroke,
pub bottom: Stroke,
pub left: Stroke,
}
pub(crate) 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);
}
pub(crate) fn gradient_texture(ctx: &egui::Context, g: BgGradient) -> egui::TextureHandle {
let key = egui::Id::new(("egui_styled::bg_gradient", g));
if let Some(handle) = ctx.data(|d| d.get_temp::<egui::TextureHandle>(key)) {
return handle;
}
let img = egui::ColorImage::new(
[2, 2],
vec![g.top_left, g.top_right, g.bottom_left, g.bottom_right],
);
let handle = ctx.load_texture("egui_styled_bg_gradient", img, egui::TextureOptions::LINEAR);
ctx.data_mut(|d| d.insert_temp(key, handle.clone()));
handle
}
pub(crate) fn bg_gradient_shape(
ctx: &egui::Context,
rect: Rect,
cr: CornerRadius,
g: BgGradient,
) -> Shape {
use egui::epaint::RectShape;
let tex = gradient_texture(ctx, g);
let uv = Rect::from_min_max(pos2(0.25, 0.25), pos2(0.75, 0.75));
Shape::Rect(RectShape::filled(rect, cr, Color32::WHITE).with_texture(tex.id(), uv))
}
const LINEAR_RAMP_TEXELS: usize = 256;
pub(crate) fn linear_gradient_texture(
ctx: &egui::Context,
g: &LinearGradient,
) -> egui::TextureHandle {
let key = egui::Id::new(("egui_styled::linear_gradient", g));
if let Some(handle) = ctx.data(|d| d.get_temp::<egui::TextureHandle>(key)) {
return handle;
}
let n = LINEAR_RAMP_TEXELS;
let pixels: Vec<Color32> = (0..n)
.map(|i| g.sample((i as f32 + 0.5) / n as f32))
.collect();
let size = match g.axis {
GradientAxis::Vertical => [1, n],
GradientAxis::Horizontal => [n, 1],
};
let img = egui::ColorImage::new(size, pixels);
let handle = ctx.load_texture(
"egui_styled_linear_gradient",
img,
egui::TextureOptions::LINEAR,
);
ctx.data_mut(|d| d.insert_temp(key, handle.clone()));
handle
}
pub(crate) fn linear_gradient_shape(
ctx: &egui::Context,
rect: Rect,
cr: CornerRadius,
g: &LinearGradient,
) -> Shape {
use egui::epaint::RectShape;
let tex = linear_gradient_texture(ctx, g);
let inset = 0.5 / LINEAR_RAMP_TEXELS as f32;
let uv = match g.axis {
GradientAxis::Vertical => Rect::from_min_max(pos2(0.0, inset), pos2(1.0, 1.0 - inset)),
GradientAxis::Horizontal => Rect::from_min_max(pos2(inset, 0.0), pos2(1.0 - inset, 1.0)),
};
Shape::Rect(RectShape::filled(rect, cr, Color32::WHITE).with_texture(tex.id(), uv))
}
pub(crate) fn gradient_shape(
ctx: &egui::Context,
rect: Rect,
cr: CornerRadius,
g: &Gradient,
) -> Shape {
match g {
Gradient::Corners(c) => bg_gradient_shape(ctx, rect, cr, *c),
Gradient::Linear(l) => linear_gradient_shape(ctx, rect, cr, l),
}
}
fn ring_mesh(
outer: Rect,
inner: Rect,
outer_color: impl Fn(egui::Pos2) -> Color32,
inner_color: impl Fn(egui::Pos2) -> Color32,
) -> Mesh {
let mut mesh = Mesh::default();
let o = [
outer.left_top(),
outer.right_top(),
outer.right_top(),
outer.right_bottom(),
outer.right_bottom(),
outer.left_bottom(),
outer.left_bottom(),
outer.left_top(),
];
let i = [
inner.left_top(),
inner.right_top(),
inner.right_top(),
inner.right_bottom(),
inner.right_bottom(),
inner.left_bottom(),
inner.left_bottom(),
inner.left_top(),
];
for p in &o {
mesh.colored_vertex(*p, outer_color(*p));
}
for p in &i {
mesh.colored_vertex(*p, inner_color(*p));
}
let quads: [(u32, u32); 4] = [(0, 1), (2, 3), (4, 5), (6, 7)];
for (a, b) in quads {
let ia = a + 8;
let ib = b + 8;
mesh.add_triangle(a, b, ib);
mesh.add_triangle(a, ib, ia);
}
mesh
}
fn rounded_ring_mesh(
rect: Rect,
cr: CornerRadius,
width: f32,
outer_color: impl Fn(egui::Pos2) -> Color32,
inner_color: impl Fn(egui::Pos2) -> Color32,
) -> Mesh {
use std::f32::consts::{FRAC_PI_2, PI};
let inner = rect.shrink(width);
let max_r = (rect.width().min(rect.height()) * 0.5).max(0.0);
let corners = [
(
cr.nw,
rect.min.x,
rect.min.y,
inner.min.x,
inner.min.y,
1.0,
1.0,
PI,
),
(
cr.ne,
rect.max.x,
rect.min.y,
inner.max.x,
inner.min.y,
-1.0,
1.0,
PI + FRAC_PI_2,
),
(
cr.se,
rect.max.x,
rect.max.y,
inner.max.x,
inner.max.y,
-1.0,
-1.0,
PI + 2.0 * FRAC_PI_2,
),
(
cr.sw,
rect.min.x,
rect.max.y,
inner.min.x,
inner.max.y,
1.0,
-1.0,
PI + 3.0 * FRAC_PI_2,
),
];
let mut outer_pts: Vec<egui::Pos2> = Vec::new();
let mut inner_pts: Vec<egui::Pos2> = Vec::new();
for (r_field, ox, oy, ix, iy, sx, sy, a0) in corners {
let r_o = (r_field as f32).clamp(0.0, max_r);
let r_i = (r_o - width).max(0.0);
let oc = egui::pos2(ox + sx * r_o, oy + sy * r_o);
let ic = egui::pos2(ix + sx * r_i, iy + sy * r_i);
let segs = if r_o < 0.5 {
0
} else {
(r_o * 0.5).ceil().clamp(1.0, 16.0) as usize
};
for k in 0..=segs {
let a = a0 + FRAC_PI_2 * (k as f32 / segs.max(1) as f32);
let dir = egui::vec2(a.cos(), a.sin());
outer_pts.push(oc + r_o * dir);
inner_pts.push(ic + r_i * dir);
}
}
let n = outer_pts.len() as u32;
let mut mesh = Mesh::default();
for p in &outer_pts {
mesh.colored_vertex(*p, outer_color(*p));
}
for p in &inner_pts {
mesh.colored_vertex(*p, inner_color(*p));
}
for k in 0..n {
let next = (k + 1) % n;
let (oa, ob) = (k, next);
let (ia, ib) = (n + k, n + next);
mesh.add_triangle(oa, ob, ib);
mesh.add_triangle(oa, ib, ia);
}
mesh
}
fn push_glow_band(mesh: &mut Mesh, rect: Rect, side: Sides, w: f32, color: Color32) {
let (oa, ob, ia, ib) = if side == Sides::TOP {
(
rect.left_top(),
rect.right_top(),
egui::pos2(rect.left(), rect.top() + w),
egui::pos2(rect.right(), rect.top() + w),
)
} else if side == Sides::BOTTOM {
(
rect.left_bottom(),
rect.right_bottom(),
egui::pos2(rect.left(), rect.bottom() - w),
egui::pos2(rect.right(), rect.bottom() - w),
)
} else if side == Sides::LEFT {
(
rect.left_top(),
rect.left_bottom(),
egui::pos2(rect.left() + w, rect.top()),
egui::pos2(rect.left() + w, rect.bottom()),
)
} else {
(
rect.right_top(),
rect.right_bottom(),
egui::pos2(rect.right() - w, rect.top()),
egui::pos2(rect.right() - w, rect.bottom()),
)
};
let base = mesh.vertices.len() as u32;
mesh.colored_vertex(oa, color);
mesh.colored_vertex(ob, color);
mesh.colored_vertex(ia, Color32::TRANSPARENT);
mesh.colored_vertex(ib, Color32::TRANSPARENT);
mesh.add_triangle(base, base + 1, base + 3);
mesh.add_triangle(base, base + 3, base + 2);
}
pub(crate) fn inner_glow_shape(rect: Rect, cr: CornerRadius, glow: InnerGlow) -> Option<Shape> {
let max_w = (rect.width().min(rect.height()) / 2.0).max(0.0);
let w = glow.width.clamp(0.0, max_w);
if w < 0.5 || glow.color.a() == 0 || !glow.sides.any() {
return None;
}
let edge = glow.color;
if glow.sides.is_all() {
let mesh = rounded_ring_mesh(rect, cr, w, |_| edge, |_| Color32::TRANSPARENT);
return Some(Shape::Mesh(mesh.into()));
}
let mut mesh = Mesh::default();
if glow.sides.top {
push_glow_band(&mut mesh, rect, Sides::TOP, w, edge);
}
if glow.sides.bottom {
push_glow_band(&mut mesh, rect, Sides::BOTTOM, w, edge);
}
if glow.sides.left {
push_glow_band(&mut mesh, rect, Sides::LEFT, w, edge);
}
if glow.sides.right {
push_glow_band(&mut mesh, rect, Sides::RIGHT, w, edge);
}
Some(Shape::Mesh(mesh.into()))
}
pub(crate) fn border_gradient_mesh(rect: Rect, g: BorderGradient) -> Mesh {
let w = g
.width
.min(rect.width() / 2.0)
.min(rect.height() / 2.0)
.max(0.0);
let inner = rect.shrink(w);
let col = move |p: egui::Pos2| {
let t = ((p.y - rect.top()) / rect.height().max(1.0)).clamp(0.0, 1.0);
g.top.lerp_to_gamma(g.bottom, t)
};
ring_mesh(rect, inner, col, col)
}
pub(crate) fn paint_widget_gradient_underlay(
ui: &egui::Ui,
slot: egui::layers::ShapeIdx,
rect: Rect,
cr: CornerRadius,
state: &ResolvedStyle,
) {
if let Some(g) = &state.bg_gradient {
let shapes = vec![
Shape::rect_filled(rect, cr, state.bg),
gradient_shape(ui.ctx(), rect, cr, g),
];
ui.painter().set(slot, Shape::Vec(shapes));
}
}
pub(crate) fn paint_widget_overlays(ui: &egui::Ui, rect: Rect, state: &ResolvedStyle) {
if let Some(bg) = state.border_gradient {
ui.painter()
.add(Shape::Mesh(border_gradient_mesh(rect, bg).into()));
} else if state.has_border_overrides {
paint_side_borders(ui.painter(), rect, state.border_sides);
}
if let Some(glow) = state.inner_glow
&& let Some(shape) = inner_glow_shape(rect, state.corner_radius, glow)
{
ui.painter().add(shape);
}
}
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(crate) 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
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Distribution {
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
pub(crate) fn distribution_spacing(
mode: Distribution,
slack: f32,
n: usize,
min_gap: f32,
) -> (f32, f32) {
if n == 0 {
return (0.0, min_gap);
}
if slack <= 0.0 {
return (0.0, min_gap);
}
let nf = n as f32;
let (l, g) = match mode {
Distribution::SpaceBetween => {
if n <= 1 {
(0.0, 0.0)
} else {
(0.0, slack / (nf - 1.0))
}
}
Distribution::SpaceAround => (slack / (2.0 * nf), slack / nf),
Distribution::SpaceEvenly => (slack / (nf + 1.0), slack / (nf + 1.0)),
};
(l, g.max(min_gap))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn distribute_row_horizontally(
ui: &mut egui::Ui,
avail: f32,
mode: Distribution,
min_gap: f32,
n: usize,
cross_align: egui::Align,
content_w_id: egui::Id,
render_items: impl FnOnce(&mut egui::Ui),
) {
let cached_w = ui.ctx().memory(|m| m.data.get_temp::<f32>(content_w_id));
let layout = egui::Layout::left_to_right(cross_align);
let initial_size = egui::vec2(avail, ui.spacing().interact_size.y);
if let Some(w) = cached_w {
let slack = (avail - w).max(0.0);
let (leading, gap) = distribution_spacing(mode, slack, n, min_gap);
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
if leading > 0.0 {
ui.add_space(leading);
}
ui.spacing_mut().item_spacing.x = gap;
render_items(ui);
});
} else {
let scope = ui.scope(|ui| {
ui.set_invisible();
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
render_items(ui);
ui.min_rect().width()
})
.inner
});
let measured_w = scope.inner;
ui.ctx()
.memory_mut(|m| m.data.insert_temp(content_w_id, measured_w));
ui.ctx().request_repaint();
}
}
pub(crate) 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(crate) 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(crate) 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 focus_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(crate) border_sides: SideStrokes,
pub(crate) hover_border_sides: SideStrokes,
pub(crate) 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 width_pct: Option<f32>,
pub height_pct: Option<f32>,
pub aspect_ratio: Option<f32>,
pub cursor_icon: Option<CursorIcon>,
pub visible: Option<bool>,
pub bg_gradient: Option<Gradient>,
pub hover_bg_gradient: Option<Gradient>,
pub active_bg_gradient: Option<Gradient>,
pub focus_bg_gradient: Option<Gradient>,
pub inner_glow: Option<InnerGlow>,
pub hover_inner_glow: Option<InnerGlow>,
pub active_inner_glow: Option<InnerGlow>,
pub focus_inner_glow: Option<InnerGlow>,
pub border_gradient: Option<BorderGradient>,
pub hover_border_gradient: Option<BorderGradient>,
pub active_border_gradient: Option<BorderGradient>,
pub focus_border_gradient: Option<BorderGradient>,
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,
}
pub struct ResolvedSize {
pub definite_w: Option<f32>,
pub definite_h: Option<f32>,
pub min_w: Option<f32>,
pub max_w: Option<f32>,
pub min_h: Option<f32>,
pub max_h: Option<f32>,
}
impl ResolvedSize {
pub fn apply_to_ui(&self, ui: &mut egui::Ui) {
if let Some(w) = self.definite_w {
ui.set_min_width(w);
ui.set_max_width(w);
} else {
if let Some(w) = self.max_w {
ui.set_max_width(w);
}
if let Some(w) = self.min_w {
ui.set_min_width(w);
}
}
if let Some(h) = self.definite_h {
ui.set_min_height(h);
ui.set_max_height(h);
} else {
if let Some(h) = self.max_h {
ui.set_max_height(h);
}
if let Some(h) = self.min_h {
ui.set_min_height(h);
}
}
}
}
#[derive(Clone)]
pub(crate) 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 bg_gradient: Option<Gradient>,
pub inner_glow: Option<InnerGlow>,
pub border_gradient: Option<BorderGradient>,
}
#[derive(Clone)]
pub(crate) 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 resolved_width_pct(&self, avail: f32) -> Option<f32> {
self.width_pct.map(|p| {
let mut w = avail * p / 100.0;
if let Some(m) = self.max_width {
w = w.min(m);
}
if let Some(m) = self.min_width {
w = w.max(m);
}
w
})
}
pub fn resolved_aspect_height(&self, width: f32) -> Option<f32> {
self.aspect_ratio.filter(|&r| r > 0.0).map(|r| {
let mut h = width / r;
if let Some(m) = self.max_height {
h = h.min(m);
}
if let Some(m) = self.min_height {
h = h.max(m);
}
h
})
}
pub fn resolved_height_pct(&self, avail: f32) -> Option<f32> {
self.height_pct.map(|p| {
let mut h = avail * p / 100.0;
if let Some(m) = self.max_height {
h = h.min(m);
}
if let Some(m) = self.min_height {
h = h.max(m);
}
h
})
}
pub fn resolve_size(&self, avail_w: f32, avail_h: f32) -> ResolvedSize {
let avail_w = if avail_w.is_finite() { avail_w } else { 0.0 };
let avail_h = if avail_h.is_finite() { avail_h } else { 0.0 };
let definite_w = if let Some(w) = self.resolved_width_pct(avail_w) {
Some(w)
} else if self.full_width && avail_w > 0.0 {
let w = self.max_width.map_or(avail_w, |m| avail_w.min(m));
Some(self.min_width.map_or(w, |m| w.max(m)))
} else {
None
};
let definite_h = if let Some(h) = self.resolved_height_pct(avail_h) {
Some(h)
} else if let Some(dw) = definite_w {
self.resolved_aspect_height(dw)
} else if self.full_height && avail_h > 0.0 {
let h = self.max_height.map_or(avail_h, |m| avail_h.min(m));
Some(self.min_height.map_or(h, |m| h.max(m)))
} else {
None
};
ResolvedSize {
definite_w,
definite_h,
min_w: if definite_w.is_none() {
self.min_width
} else {
None
},
max_w: if definite_w.is_none() {
self.max_width
} else {
None
},
min_h: if definite_h.is_none() {
self.min_height
} else {
None
},
max_h: if definite_h.is_none() {
self.max_height
} else {
None
},
}
}
pub(crate) 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.focused && self.focus_text_color.is_some() => self.focus_text_color.unwrap(),
_ if state.hovered && self.hover_text_color.is_some() => self.hover_text_color.unwrap(),
_ => self.text_color.unwrap_or(default.text_color()),
};
let bg_gradient = match state {
_ if state.active && self.active_bg_gradient.is_some() => {
self.active_bg_gradient.clone()
}
_ if state.hovered && self.hover_bg_gradient.is_some() => {
self.hover_bg_gradient.clone()
}
_ if state.focused && self.focus_bg_gradient.is_some() => {
self.focus_bg_gradient.clone()
}
_ => self.bg_gradient.clone(),
};
let inner_glow = match state {
_ if state.active && self.active_inner_glow.is_some() => self.active_inner_glow,
_ if state.hovered && self.hover_inner_glow.is_some() => self.hover_inner_glow,
_ if state.focused && self.focus_inner_glow.is_some() => self.focus_inner_glow,
_ => self.inner_glow,
};
let border_gradient = match state {
_ if state.active && self.active_border_gradient.is_some() => {
self.active_border_gradient
}
_ if state.hovered && self.hover_border_gradient.is_some() => {
self.hover_border_gradient
}
_ if state.focused && self.focus_border_gradient.is_some() => {
self.focus_border_gradient
}
_ => self.border_gradient,
};
ResolvedStyle {
bg,
text_color,
border,
border_sides,
has_border_overrides,
corner_radius: self.corner_radius.unwrap_or(default.corner_radius),
bg_gradient,
inner_glow,
border_gradient,
}
}
pub(crate) 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(crate) fn apply_to_visuals(per: &PerStateStyle, pseudo: PseudoState, 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 = if resolved.bg_gradient.is_some() {
Color32::TRANSPARENT
} else {
resolved.bg
};
wv.weak_bg_fill = wv.bg_fill;
wv.bg_stroke = if resolved.border_gradient.is_some() || 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);
}
let open_bg = if per.inactive.bg_gradient.is_some() {
Color32::TRANSPARENT
} else {
per.inactive.bg
};
vis.widgets.open.bg_fill = open_bg;
vis.widgets.open.weak_bg_fill = open_bg;
vis.widgets.open.bg_stroke =
if per.inactive.border_gradient.is_some() || 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 = if pseudo.hovered {
per.hover_accent
} else {
per.accent
};
vis.selection.stroke = per.focused.border;
}
pub(crate) 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(crate) 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()
|| self.bg_gradient.is_some()
|| self.hover_bg_gradient.is_some()
|| self.active_bg_gradient.is_some()
|| self.focus_bg_gradient.is_some()
|| self.inner_glow.is_some()
|| self.hover_inner_glow.is_some()
|| self.active_inner_glow.is_some()
|| self.focus_inner_glow.is_some()
|| self.border_gradient.is_some()
|| self.hover_border_gradient.is_some()
|| self.active_border_gradient.is_some()
|| self.focus_border_gradient.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 focus_text_color_beats_hover_text_color() {
let style = SharedStyle {
text_color: Some(Color32::RED),
hover_text_color: Some(Color32::GREEN),
focus_text_color: Some(Color32::YELLOW),
..Default::default()
};
let visuals = default_visuals();
let state = PseudoState {
hovered: true,
active: false,
focused: true,
};
assert_eq!(style.resolve(state, &visuals).text_color, Color32::YELLOW);
let hovered_only = PseudoState {
hovered: true,
active: false,
focused: false,
};
assert_eq!(
style.resolve(hovered_only, &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);
}
#[test]
fn resolved_width_pct_none_when_unset() {
let style = SharedStyle::default();
assert!(style.resolved_width_pct(400.0).is_none());
}
#[test]
fn resolved_width_pct_half_of_avail() {
let style = SharedStyle {
width_pct: Some(50.0),
..Default::default()
};
let w = style.resolved_width_pct(400.0).unwrap();
assert!((w - 200.0).abs() < 1e-4, "50% of 400 = 200, got {w}");
}
#[test]
fn resolved_width_pct_clamped_by_max_width() {
let style = SharedStyle {
width_pct: Some(50.0),
max_width: Some(120.0),
..Default::default()
};
let w = style.resolved_width_pct(400.0).unwrap();
assert!(
(w - 120.0).abs() < 1e-4,
"50% of 400 clamped to max_width 120, got {w}"
);
}
#[test]
fn resolved_width_pct_floored_by_min_width() {
let style = SharedStyle {
width_pct: Some(10.0),
min_width: Some(80.0),
..Default::default()
};
let w = style.resolved_width_pct(400.0).unwrap();
assert!(
(w - 80.0).abs() < 1e-4,
"10% of 400=40 raised to min_width 80, got {w}"
);
}
#[test]
fn resolved_height_pct_half_of_avail() {
let style = SharedStyle {
height_pct: Some(50.0),
..Default::default()
};
let h = style.resolved_height_pct(300.0).unwrap();
assert!((h - 150.0).abs() < 1e-4, "50% of 300 = 150, got {h}");
}
#[test]
fn dist_between_equal_gaps() {
let (l, g) = distribution_spacing(Distribution::SpaceBetween, 300.0, 3, 0.0);
assert!((l - 0.0).abs() < 1e-4, "between: L should be 0, got {l}");
assert!(
(g - 150.0).abs() < 1e-4,
"between: G should be 150, got {g}"
);
}
#[test]
fn dist_around_equal_margin() {
let (l, g) = distribution_spacing(Distribution::SpaceAround, 300.0, 3, 0.0);
assert!((l - 50.0).abs() < 1e-4, "around: L should be 50, got {l}");
assert!((g - 100.0).abs() < 1e-4, "around: G should be 100, got {g}");
}
#[test]
fn dist_evenly_equal_everywhere() {
let (l, g) = distribution_spacing(Distribution::SpaceEvenly, 400.0, 3, 0.0);
assert!((l - 100.0).abs() < 1e-4, "evenly: L should be 100, got {l}");
assert!((g - 100.0).abs() < 1e-4, "evenly: G should be 100, got {g}");
}
#[test]
fn dist_single_item_centers_for_around_evenly() {
let (l_around, _) = distribution_spacing(Distribution::SpaceAround, 200.0, 1, 0.0);
let (l_evenly, _) = distribution_spacing(Distribution::SpaceEvenly, 200.0, 1, 0.0);
assert!(
(l_around - 100.0).abs() < 1e-4,
"around n=1: L=100, got {l_around}"
);
assert!(
(l_evenly - 100.0).abs() < 1e-4,
"evenly n=1: L=100, got {l_evenly}"
);
}
#[test]
fn dist_zero_slack_uses_min_gap() {
let (l, g) = distribution_spacing(Distribution::SpaceBetween, 0.0, 3, 8.0);
assert!((l - 0.0).abs() < 1e-4, "zero slack: L=0, got {l}");
assert!((g - 8.0).abs() < 1e-4, "zero slack: G=min_gap=8, got {g}");
}
#[test]
fn resolved_aspect_height_none_when_unset() {
let s = SharedStyle::default();
assert!(s.resolved_aspect_height(200.0).is_none());
}
#[test]
fn resolved_aspect_height_none_when_ratio_zero_or_negative() {
let s = SharedStyle {
aspect_ratio: Some(0.0),
..Default::default()
};
assert!(s.resolved_aspect_height(200.0).is_none());
let s2 = SharedStyle {
aspect_ratio: Some(-1.0),
..Default::default()
};
assert!(s2.resolved_aspect_height(200.0).is_none());
}
#[test]
fn resolved_aspect_height_basic() {
let s = SharedStyle {
aspect_ratio: Some(2.0),
..Default::default()
};
assert!((s.resolved_aspect_height(200.0).unwrap() - 100.0).abs() < 1e-4);
}
#[test]
fn resolved_aspect_height_16_9() {
let s = SharedStyle {
aspect_ratio: Some(16.0 / 9.0),
..Default::default()
};
let h = s.resolved_aspect_height(320.0).unwrap();
assert!((h - 180.0).abs() < 1e-3, "16:9 of 320 → 180, got {h}");
}
#[test]
fn resolved_aspect_height_clamped_by_max() {
let s = SharedStyle {
aspect_ratio: Some(1.0),
max_height: Some(50.0),
..Default::default()
};
assert!((s.resolved_aspect_height(200.0).unwrap() - 50.0).abs() < 1e-4);
}
#[test]
fn resolved_aspect_height_floored_by_min() {
let s = SharedStyle {
aspect_ratio: Some(10.0),
min_height: Some(80.0),
..Default::default()
};
assert!((s.resolved_aspect_height(200.0).unwrap() - 80.0).abs() < 1e-4);
}
#[test]
fn resolve_size_width_pct_50_on_400() {
let s = SharedStyle {
width_pct: Some(50.0),
..Default::default()
};
let sz = s.resolve_size(400.0, 300.0);
assert!((sz.definite_w.unwrap() - 200.0).abs() < 1e-4);
assert!(sz.definite_h.is_none());
}
#[test]
fn resolve_size_aspect_from_pct() {
let s = SharedStyle {
width_pct: Some(50.0),
aspect_ratio: Some(2.0),
..Default::default()
};
let sz = s.resolve_size(400.0, 300.0);
assert!((sz.definite_w.unwrap() - 200.0).abs() < 1e-4);
assert!((sz.definite_h.unwrap() - 100.0).abs() < 1e-4);
}
#[test]
fn resolve_size_non_finite_avail_produces_no_definite() {
let s = SharedStyle {
full_width: true,
..Default::default()
};
let sz = s.resolve_size(f32::INFINITY, f32::INFINITY);
assert!(sz.definite_w.is_none());
assert!(sz.definite_h.is_none());
}
fn style_with_all_gradients() -> SharedStyle {
SharedStyle {
bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::RED,
Color32::BLUE,
))),
hover_bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::GREEN,
Color32::BLUE,
))),
active_bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::YELLOW,
Color32::BLUE,
))),
focus_bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::WHITE,
Color32::BLUE,
))),
..Default::default()
}
}
#[test]
fn bg_gradient_active_beats_hovered_and_focused() {
let style = style_with_all_gradients();
let resolved = style.resolve(
PseudoState {
hovered: true,
active: true,
focused: true,
},
&default_visuals(),
);
assert_eq!(
resolved.bg_gradient,
Some(Gradient::Corners(BgGradient::vertical(
Color32::YELLOW,
Color32::BLUE
)))
);
}
#[test]
fn bg_gradient_hover_beats_focused() {
let style = style_with_all_gradients();
let resolved = style.resolve(
PseudoState {
hovered: true,
active: false,
focused: true,
},
&default_visuals(),
);
assert_eq!(
resolved.bg_gradient,
Some(Gradient::Corners(BgGradient::vertical(
Color32::GREEN,
Color32::BLUE
)))
);
}
#[test]
fn bg_gradient_focus_only() {
let style = style_with_all_gradients();
let resolved = style.resolve(
PseudoState {
hovered: false,
active: false,
focused: true,
},
&default_visuals(),
);
assert_eq!(
resolved.bg_gradient,
Some(Gradient::Corners(BgGradient::vertical(
Color32::WHITE,
Color32::BLUE
)))
);
}
#[test]
fn bg_gradient_falls_back_to_base() {
let style = style_with_all_gradients();
let resolved = style.resolve(PseudoState::default(), &default_visuals());
assert_eq!(
resolved.bg_gradient,
Some(Gradient::Corners(BgGradient::vertical(
Color32::RED,
Color32::BLUE
)))
);
}
fn style_with_all_glows() -> SharedStyle {
SharedStyle {
inner_glow: Some(InnerGlow::new(4.0, Color32::RED)),
hover_inner_glow: Some(InnerGlow::new(8.0, Color32::GREEN)),
active_inner_glow: Some(InnerGlow::new(12.0, Color32::YELLOW)),
focus_inner_glow: Some(InnerGlow::new(6.0, Color32::WHITE)),
..Default::default()
}
}
#[test]
fn inner_glow_active_beats_hovered_and_focused() {
let style = style_with_all_glows();
let resolved = style.resolve(
PseudoState {
hovered: true,
active: true,
focused: true,
},
&default_visuals(),
);
assert_eq!(
resolved.inner_glow,
Some(InnerGlow::new(12.0, Color32::YELLOW))
);
}
#[test]
fn inner_glow_hover_beats_focused() {
let style = style_with_all_glows();
let resolved = style.resolve(
PseudoState {
hovered: true,
active: false,
focused: true,
},
&default_visuals(),
);
assert_eq!(
resolved.inner_glow,
Some(InnerGlow::new(8.0, Color32::GREEN))
);
}
#[test]
fn inner_glow_falls_back_to_base() {
let style = style_with_all_glows();
let resolved = style.resolve(PseudoState::default(), &default_visuals());
assert_eq!(resolved.inner_glow, Some(InnerGlow::new(4.0, Color32::RED)));
}
fn style_with_all_border_gradients() -> SharedStyle {
SharedStyle {
border_gradient: Some(BorderGradient::new(2.0, Color32::RED, Color32::BLUE)),
hover_border_gradient: Some(BorderGradient::new(3.0, Color32::GREEN, Color32::BLUE)),
active_border_gradient: Some(BorderGradient::new(4.0, Color32::YELLOW, Color32::BLUE)),
focus_border_gradient: Some(BorderGradient::new(2.5, Color32::WHITE, Color32::BLUE)),
..Default::default()
}
}
#[test]
fn border_gradient_active_beats_hovered_and_focused() {
let style = style_with_all_border_gradients();
let resolved = style.resolve(
PseudoState {
hovered: true,
active: true,
focused: true,
},
&default_visuals(),
);
assert_eq!(
resolved.border_gradient,
Some(BorderGradient::new(4.0, Color32::YELLOW, Color32::BLUE))
);
}
#[test]
fn border_gradient_falls_back_to_base() {
let style = style_with_all_border_gradients();
let resolved = style.resolve(PseudoState::default(), &default_visuals());
assert_eq!(
resolved.border_gradient,
Some(BorderGradient::new(2.0, Color32::RED, Color32::BLUE))
);
}
#[test]
fn has_frame_styles_triggers_for_gradient_fields() {
for style in [
SharedStyle {
bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::RED,
Color32::BLUE,
))),
..Default::default()
},
SharedStyle {
hover_bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::RED,
Color32::BLUE,
))),
..Default::default()
},
SharedStyle {
inner_glow: Some(InnerGlow::new(4.0, Color32::RED)),
..Default::default()
},
SharedStyle {
hover_inner_glow: Some(InnerGlow::new(4.0, Color32::RED)),
..Default::default()
},
SharedStyle {
border_gradient: Some(BorderGradient::new(2.0, Color32::RED, Color32::BLUE)),
..Default::default()
},
SharedStyle {
hover_border_gradient: Some(BorderGradient::new(2.0, Color32::RED, Color32::BLUE)),
..Default::default()
},
] {
assert!(style.has_frame_styles());
}
}
#[test]
fn apply_to_visuals_suppresses_fill_when_bg_gradient() {
use egui::Visuals;
let ctx = egui::Context::default();
let visuals = Visuals::default();
let style = SharedStyle {
bg_gradient: Some(Gradient::Corners(BgGradient::vertical(
Color32::RED,
Color32::BLUE,
))),
..Default::default()
};
let per = style.resolve_per_state(&visuals);
let mut vis = visuals.clone();
SharedStyle::apply_to_visuals(&per, PseudoState::default(), &mut vis);
assert_eq!(vis.widgets.inactive.bg_fill, Color32::TRANSPARENT);
assert_eq!(vis.widgets.inactive.weak_bg_fill, Color32::TRANSPARENT);
let _ = ctx;
}
#[test]
fn apply_to_visuals_suppresses_stroke_when_border_gradient() {
use egui::Visuals;
let visuals = Visuals::default();
let style = SharedStyle {
border_gradient: Some(BorderGradient::new(2.0, Color32::RED, Color32::BLUE)),
..Default::default()
};
let per = style.resolve_per_state(&visuals);
let mut vis = visuals.clone();
SharedStyle::apply_to_visuals(&per, PseudoState::default(), &mut vis);
assert_eq!(vis.widgets.inactive.bg_stroke, Stroke::NONE);
}
#[test]
fn apply_to_visuals_selects_hover_accent_when_pseudo_hovered() {
use egui::Visuals;
let visuals = Visuals::default();
let style = SharedStyle {
accent: Some(Color32::RED),
hover_accent: Some(Color32::GREEN),
..Default::default()
};
let per = style.resolve_per_state(&visuals);
let mut vis_base = visuals.clone();
SharedStyle::apply_to_visuals(&per, PseudoState::default(), &mut vis_base);
assert_eq!(vis_base.selection.bg_fill, Color32::RED);
let mut vis_hovered = visuals.clone();
let hovered = PseudoState {
hovered: true,
..Default::default()
};
SharedStyle::apply_to_visuals(&per, hovered, &mut vis_hovered);
assert_eq!(vis_hovered.selection.bg_fill, Color32::GREEN);
}
#[test]
fn bg_gradient_constructors() {
let v = BgGradient::vertical(Color32::RED, Color32::BLUE);
assert_eq!(v.top_left, Color32::RED);
assert_eq!(v.top_right, Color32::RED);
assert_eq!(v.bottom_left, Color32::BLUE);
assert_eq!(v.bottom_right, Color32::BLUE);
let h = BgGradient::horizontal(Color32::RED, Color32::BLUE);
assert_eq!(h.top_left, Color32::RED);
assert_eq!(h.top_right, Color32::BLUE);
assert_eq!(h.bottom_left, Color32::RED);
assert_eq!(h.bottom_right, Color32::BLUE);
}
#[test]
fn gradient_texture_cached_by_colors() {
let ctx = egui::Context::default();
ctx.begin_pass(egui::RawInput::default());
let g = BgGradient::vertical(Color32::RED, Color32::BLUE);
let h1 = gradient_texture(&ctx, g);
let h2 = gradient_texture(&ctx, g);
assert_eq!(h1.id(), h2.id());
let g2 = BgGradient::vertical(Color32::GREEN, Color32::BLUE);
let h3 = gradient_texture(&ctx, g2);
assert_ne!(h1.id(), h3.id());
}
#[test]
fn bg_gradient_shape_uses_texel_center_uv() {
let ctx = egui::Context::default();
ctx.begin_pass(egui::RawInput::default());
let g = BgGradient::vertical(Color32::RED, Color32::BLUE);
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let shape = bg_gradient_shape(&ctx, rect, CornerRadius::default(), g);
match shape {
Shape::Rect(rs) => {
let brush = rs.brush.expect("gradient shape must have a brush");
let expected = egui::Rect::from_min_max(pos2(0.25, 0.25), pos2(0.75, 0.75));
assert!((brush.uv.min.x - expected.min.x).abs() < 1e-5);
assert!((brush.uv.min.y - expected.min.y).abs() < 1e-5);
assert!((brush.uv.max.x - expected.max.x).abs() < 1e-5);
assert!((brush.uv.max.y - expected.max.y).abs() < 1e-5);
}
other => panic!("expected Shape::Rect, got {:?}", other),
}
}
#[test]
fn linear_gradient_sorts_and_samples_stops() {
let g = LinearGradient::new(
[(1.0, Color32::BLUE), (0.0, Color32::RED)],
GradientAxis::Vertical,
);
assert_eq!(g.stops[0].1, Color32::RED);
assert_eq!(g.stops[1].1, Color32::BLUE);
assert_eq!(g.sample(0.0), Color32::RED);
assert_eq!(g.sample(1.0), Color32::BLUE);
let mid = g.sample(0.5);
assert!(mid != Color32::RED && mid != Color32::BLUE);
}
#[test]
fn linear_gradient_clamps_outside_range() {
let g = LinearGradient::new(
[(0.25, Color32::RED), (0.75, Color32::BLUE)],
GradientAxis::Vertical,
);
assert_eq!(g.sample(0.0), Color32::RED); assert_eq!(g.sample(1.0), Color32::BLUE); }
#[test]
fn linear_gradient_evenly_spaced_positions() {
let g = LinearGradient::evenly_spaced(
[Color32::RED, Color32::GREEN, Color32::BLUE],
GradientAxis::Horizontal,
);
assert_eq!(g.stops.len(), 3);
assert!((g.stops[0].0 - 0.0).abs() < 1e-6);
assert!((g.stops[1].0 - 0.5).abs() < 1e-6);
assert!((g.stops[2].0 - 1.0).abs() < 1e-6);
}
#[test]
fn linear_gradient_texture_cached_and_axis_sized() {
let ctx = egui::Context::default();
ctx.begin_pass(egui::RawInput::default());
let v = LinearGradient::new(
[(0.0, Color32::RED), (1.0, Color32::BLUE)],
GradientAxis::Vertical,
);
let h1 = linear_gradient_texture(&ctx, &v);
let h2 = linear_gradient_texture(&ctx, &v);
assert_eq!(h1.id(), h2.id());
let h = LinearGradient::new(
[(0.0, Color32::RED), (1.0, Color32::BLUE)],
GradientAxis::Horizontal,
);
let h3 = linear_gradient_texture(&ctx, &h);
assert_ne!(h1.id(), h3.id());
}
#[test]
fn linear_gradient_shape_axis_uv() {
let ctx = egui::Context::default();
ctx.begin_pass(egui::RawInput::default());
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let inset = 0.5 / LINEAR_RAMP_TEXELS as f32;
let v = LinearGradient::new(
[(0.0, Color32::RED), (1.0, Color32::BLUE)],
GradientAxis::Vertical,
);
if let Shape::Rect(rs) = linear_gradient_shape(&ctx, rect, CornerRadius::default(), &v) {
let uv = rs.brush.unwrap().uv;
assert!((uv.min.x - 0.0).abs() < 1e-6 && (uv.max.x - 1.0).abs() < 1e-6);
assert!((uv.min.y - inset).abs() < 1e-6 && (uv.max.y - (1.0 - inset)).abs() < 1e-6);
} else {
panic!("expected Shape::Rect");
}
}
#[test]
fn inner_glow_per_side_emits_only_selected_bands() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let glow = InnerGlow::with_sides(8.0, Color32::WHITE, Sides::Y);
let shape = inner_glow_shape(rect, CornerRadius::default(), glow).unwrap();
if let Shape::Mesh(mesh) = shape {
assert_eq!(mesh.vertices.len(), 8);
assert_eq!(mesh.indices.len(), 12);
for v in &mesh.vertices {
let y = v.pos.y;
let on_band = y < 1e-3
|| (y - 8.0).abs() < 1e-3
|| (y - 92.0).abs() < 1e-3
|| (y - 100.0).abs() < 1e-3;
assert!(on_band, "vertex y={y} not on a top/bottom band");
}
} else {
panic!("expected Shape::Mesh");
}
}
#[test]
fn inner_glow_no_sides_emits_nothing() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let none = Sides {
top: false,
right: false,
bottom: false,
left: false,
};
let glow = InnerGlow::with_sides(8.0, Color32::WHITE, none);
assert!(inner_glow_shape(rect, CornerRadius::default(), glow).is_none());
}
#[test]
fn inner_glow_shape_full_at_edge_transparent_inward() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let glow = InnerGlow::new(16.0, Color32::from_rgba_premultiplied(255, 255, 255, 200));
let shape = inner_glow_shape(rect, CornerRadius::default(), glow)
.expect("glow should emit a shape");
match shape {
Shape::Mesh(mesh) => {
assert!(mesh.vertices.len() >= 8);
assert_eq!(mesh.vertices.len() % 2, 0);
let half = mesh.vertices.len() / 2;
for v in &mesh.vertices[0..half] {
assert_eq!(v.color, glow.color);
}
for v in &mesh.vertices[half..] {
assert_eq!(v.color, Color32::TRANSPARENT);
}
}
other => panic!("expected Shape::Mesh, got {:?}", other),
}
}
#[test]
fn inner_glow_rounded_corners_add_vertices() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let glow = InnerGlow::new(8.0, Color32::WHITE);
let sharp = inner_glow_shape(rect, CornerRadius::default(), glow).unwrap();
let rounded = inner_glow_shape(rect, CornerRadius::same(20), glow).unwrap();
let (sharp_n, rounded_n) = match (sharp, rounded) {
(Shape::Mesh(a), Shape::Mesh(b)) => (a.vertices.len(), b.vertices.len()),
_ => panic!("expected meshes"),
};
assert!(rounded_n > sharp_n);
}
#[test]
fn inner_glow_zero_width_emits_nothing() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
assert!(
inner_glow_shape(
rect,
CornerRadius::default(),
InnerGlow::new(0.0, Color32::RED)
)
.is_none()
);
}
#[test]
fn inner_glow_transparent_color_emits_nothing() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
assert!(
inner_glow_shape(
rect,
CornerRadius::default(),
InnerGlow::new(8.0, Color32::TRANSPARENT)
)
.is_none()
);
}
#[test]
fn inner_glow_width_clamped_to_half_rect() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::new(50.0, 30.0));
let glow = InnerGlow::new(1000.0, Color32::WHITE);
let shape = inner_glow_shape(rect, CornerRadius::default(), glow)
.expect("glow should emit a shape");
if let Shape::Mesh(mesh) = shape {
for v in &mesh.vertices {
assert!(rect.contains(v.pos) || rect.distance_to_pos(v.pos) < 1e-3);
}
} else {
panic!("expected Shape::Mesh");
}
}
#[test]
fn border_gradient_mesh_geometry() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(100.0));
let g = BorderGradient::new(10.0, Color32::RED, Color32::BLUE);
let mesh = border_gradient_mesh(rect, g);
assert_eq!(mesh.vertices.len(), 16);
assert_eq!(mesh.indices.len(), 24);
assert_eq!(mesh.vertices[0].color, Color32::RED);
assert_eq!(mesh.vertices[1].color, Color32::RED);
assert_eq!(mesh.vertices[4].color, Color32::BLUE);
assert_eq!(mesh.vertices[5].color, Color32::BLUE);
}
#[test]
fn border_gradient_mesh_width_clamped() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(10.0));
let g = BorderGradient::new(1000.0, Color32::RED, Color32::BLUE);
let mesh = border_gradient_mesh(rect, g);
for v in &mesh.vertices {
assert!(rect.contains(v.pos) || rect.distance_to_pos(v.pos) < 1e-3);
}
}
}