#![forbid(unsafe_code)]
use egui::{Color32, CornerRadius, Margin, Painter, Rect, Shape, Stroke, Ui};
use makeover_layout::{Bevel, Depth, Edge, Fill};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Palette {
pub page: Color32,
pub raised: Color32,
pub overlay: Color32,
pub well: Color32,
pub sunken: Color32,
pub bevel_light: Color32,
pub bevel_dark: Color32,
}
impl Palette {
#[must_use]
pub const fn fill(&self, fill: Fill) -> Option<Color32> {
match fill {
Fill::Page => Some(self.page),
Fill::Raised => Some(self.raised),
Fill::Overlay => Some(self.overlay),
Fill::Well => Some(self.well),
Fill::Sunken => Some(self.sunken),
_ => None,
}
}
#[must_use]
pub const fn edge(&self, edge: Edge) -> Color32 {
match edge {
Edge::Light => self.bevel_light,
Edge::Dark => self.bevel_dark,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameStyle {
pub radius: CornerRadius,
pub margin: Margin,
pub stroke: f32,
}
impl Default for FrameStyle {
fn default() -> Self {
Self {
radius: CornerRadius::ZERO,
margin: Margin::ZERO,
stroke: 1.0,
}
}
}
pub fn paint_bevel(painter: &Painter, rect: Rect, bevel: Bevel, palette: &Palette, stroke: f32) {
let (top_left, bottom_right) = bevel.edges();
let r = rect.shrink(stroke / 2.0);
painter.add(Shape::line(
vec![r.left_bottom(), r.left_top(), r.right_top()],
Stroke::new(stroke, palette.edge(top_left)),
));
painter.add(Shape::line(
vec![r.right_top(), r.right_bottom(), r.left_bottom()],
Stroke::new(stroke, palette.edge(bottom_right)),
));
}
pub fn frame<R>(
ui: &mut Ui,
depth: Depth,
palette: &Palette,
style: FrameStyle,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> R {
let mut f = egui::Frame::new()
.corner_radius(style.radius)
.inner_margin(style.margin);
if let Some(fill) = depth.fill().and_then(|f| palette.fill(f)) {
f = f.fill(fill);
}
let framed = f.show(ui, add_contents);
if let Some(bevel) = depth.bevel() {
paint_bevel(
ui.painter(),
framed.response.rect,
bevel,
palette,
style.stroke,
);
}
framed.inner
}
#[cfg(test)]
mod tests {
use super::*;
fn palette(well: Color32) -> Palette {
Palette {
page: Color32::from_rgb(1, 1, 1),
raised: Color32::from_rgb(2, 2, 2),
overlay: Color32::from_rgb(3, 3, 3),
well,
sunken: Color32::from_rgb(4, 4, 4),
bevel_light: Color32::WHITE,
bevel_dark: Color32::BLACK,
}
}
#[test]
fn a_well_resolves_to_its_own_token() {
let w = Color32::from_rgb(9, 9, 9);
let p = palette(w);
assert_eq!(p.fill(Fill::Well), Some(w));
assert_ne!(p.fill(Fill::Well), Some(p.page));
}
#[test]
fn every_intent_is_a_plain_lookup() {
let p = palette(Color32::from_rgb(9, 9, 9));
assert_eq!(p.fill(Fill::Page), Some(p.page));
assert_eq!(p.fill(Fill::Raised), Some(p.raised));
assert_eq!(p.fill(Fill::Overlay), Some(p.overlay));
}
#[test]
fn sunken_is_neither_the_well_nor_the_page() {
let p = palette(Color32::from_rgb(9, 9, 9));
assert_eq!(p.fill(Fill::Sunken), Some(p.sunken));
assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Well));
assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Page));
}
#[test]
fn a_raised_region_never_resolves_to_the_well_fill() {
let p = palette(Color32::from_rgb(9, 9, 9));
let raised = Depth::Raised.fill().and_then(|f| p.fill(f));
let well = Depth::Well.fill().and_then(|f| p.fill(f));
assert_eq!(raised, Some(p.raised));
assert_ne!(raised, well);
}
#[test]
fn the_lit_edge_swaps_when_a_card_is_pressed() {
let p = palette(Color32::from_rgb(9, 9, 9));
let (tl, _) = Depth::Raised.bevel().unwrap().edges();
let (ptl, _) = Depth::Raised.pressed().bevel().unwrap().edges();
assert_eq!(p.edge(tl), p.bevel_light);
assert_eq!(p.edge(ptl), p.bevel_dark);
}
#[test]
fn flat_asks_for_neither_fill_nor_edge() {
assert!(Depth::Flat.fill().is_none());
assert!(Depth::Flat.bevel().is_none());
}
#[test]
fn the_default_frame_is_square_and_one_point() {
let d = FrameStyle::default();
assert_eq!(d.radius, CornerRadius::ZERO);
assert_eq!(d.margin, Margin::ZERO);
assert!((d.stroke - 1.0).abs() < f32::EPSILON);
}
}