use crate::theme::{Swatch, Tone};
macro_rules! icon {
($name:literal) => {
egui::include_image!(concat!("../assets/icons/", $name, ".svg"))
};
}
pub fn install_loaders(ctx: &egui::Context) {
egui_extras::install_image_loaders(ctx);
}
pub fn image(ui: &egui::Ui, src: egui::ImageSource<'static>, size: f32) -> egui::Image<'static> {
image_in(src, size, ui.visuals().text_color())
}
pub fn image_as(
ui: &egui::Ui,
src: egui::ImageSource<'static>,
size: f32,
tone: Tone,
swatch: Swatch,
) -> egui::Image<'static> {
image_in(
src,
size,
crate::theme::current(ui.ctx()).color(tone, swatch),
)
}
pub fn image_in(
src: egui::ImageSource<'static>,
size: f32,
colour: egui::Color32,
) -> egui::Image<'static> {
square(src, size).tint(colour)
}
pub fn paint(
ui: &egui::Ui,
rect: egui::Rect,
src: egui::ImageSource<'static>,
colour: egui::Color32,
) {
egui::Image::new(src).tint(colour).paint_at(ui, rect);
}
pub fn button(src: egui::ImageSource<'static>, size: f32) -> egui::Button<'static> {
egui::Button::image(square(src, size)).image_tint_follows_text_color(true)
}
fn square(src: egui::ImageSource<'static>, size: f32) -> egui::Image<'static> {
egui::Image::new(src).fit_to_exact_size(egui::vec2(size, size))
}
pub fn settings() -> egui::ImageSource<'static> {
icon!("settings")
}
pub fn arrow_left() -> egui::ImageSource<'static> {
icon!("arrow-left")
}
pub fn x() -> egui::ImageSource<'static> {
icon!("x")
}
pub fn plus() -> egui::ImageSource<'static> {
icon!("plus")
}
pub fn trash_2() -> egui::ImageSource<'static> {
icon!("trash-2")
}
pub fn pencil_line() -> egui::ImageSource<'static> {
icon!("pencil-line")
}
pub fn refresh_cw() -> egui::ImageSource<'static> {
icon!("refresh-cw")
}
pub fn info() -> egui::ImageSource<'static> {
icon!("info")
}
pub fn panel_right() -> egui::ImageSource<'static> {
icon!("panel-right")
}
pub fn ellipsis() -> egui::ImageSource<'static> {
icon!("ellipsis")
}
pub fn grip_vertical() -> egui::ImageSource<'static> {
icon!("grip-vertical")
}
pub fn chevron_right() -> egui::ImageSource<'static> {
icon!("chevron-right")
}
pub fn chevron_down() -> egui::ImageSource<'static> {
icon!("chevron-down")
}
pub fn chevron_up() -> egui::ImageSource<'static> {
icon!("chevron-up")
}
pub fn arrow_up() -> egui::ImageSource<'static> {
icon!("arrow-up")
}
pub fn list() -> egui::ImageSource<'static> {
icon!("list")
}
pub fn layout_grid() -> egui::ImageSource<'static> {
icon!("layout-grid")
}
pub fn circle_check() -> egui::ImageSource<'static> {
icon!("circle-check")
}
pub fn circle_x() -> egui::ImageSource<'static> {
icon!("circle-x")
}
pub fn folder() -> egui::ImageSource<'static> {
icon!("folder")
}
pub fn file_text() -> egui::ImageSource<'static> {
icon!("file-text")
}
pub fn file() -> egui::ImageSource<'static> {
icon!("file")
}
pub fn film() -> egui::ImageSource<'static> {
icon!("film")
}
pub fn rotate_ccw() -> egui::ImageSource<'static> {
icon!("rotate-ccw")
}
pub fn dice_5() -> egui::ImageSource<'static> {
icon!("dice-5")
}
pub fn picture() -> egui::ImageSource<'static> {
icon!("image")
}
pub fn images() -> egui::ImageSource<'static> {
icon!("images")
}
pub fn music() -> egui::ImageSource<'static> {
icon!("music")
}
pub fn list_music() -> egui::ImageSource<'static> {
icon!("list-music")
}
pub fn smartphone() -> egui::ImageSource<'static> {
icon!("smartphone")
}
pub fn monitor_smartphone() -> egui::ImageSource<'static> {
icon!("monitor-smartphone")
}
pub fn play() -> egui::ImageSource<'static> {
icon!("play")
}
pub fn pause() -> egui::ImageSource<'static> {
icon!("pause")
}
pub fn skip_back() -> egui::ImageSource<'static> {
icon!("skip-back")
}
pub fn skip_forward() -> egui::ImageSource<'static> {
icon!("skip-forward")
}
pub fn volume_2() -> egui::ImageSource<'static> {
icon!("volume-2")
}
pub fn volume_x() -> egui::ImageSource<'static> {
icon!("volume-x")
}
pub fn star() -> egui::ImageSource<'static> {
icon!("star")
}
pub fn tag() -> egui::ImageSource<'static> {
icon!("tag")
}
pub fn search() -> egui::ImageSource<'static> {
icon!("search")
}
pub fn clipboard_copy() -> egui::ImageSource<'static> {
icon!("clipboard-copy")
}
pub fn upload() -> egui::ImageSource<'static> {
icon!("upload")
}
pub fn download() -> egui::ImageSource<'static> {
icon!("download")
}
pub fn cloud_off() -> egui::ImageSource<'static> {
icon!("cloud-off")
}
#[cfg(test)]
mod tests {
use super::*;
type Icon = fn() -> egui::ImageSource<'static>;
const ALL: [(&str, Icon); 43] = [
("settings", settings),
("arrow_left", arrow_left),
("x", x),
("plus", plus),
("trash_2", trash_2),
("pencil_line", pencil_line),
("refresh_cw", refresh_cw),
("info", info),
("ellipsis", ellipsis),
("grip_vertical", grip_vertical),
("chevron_right", chevron_right),
("chevron_down", chevron_down),
("chevron_up", chevron_up),
("arrow_up", arrow_up),
("list", list),
("layout_grid", layout_grid),
("circle_check", circle_check),
("circle_x", circle_x),
("folder", folder),
("file_text", file_text),
("file", file),
("film", film),
("rotate_ccw", rotate_ccw),
("dice_5", dice_5),
("picture", picture),
("images", images),
("music", music),
("list_music", list_music),
("smartphone", smartphone),
("monitor_smartphone", monitor_smartphone),
("play", play),
("pause", pause),
("skip_back", skip_back),
("skip_forward", skip_forward),
("volume_2", volume_2),
("volume_x", volume_x),
("star", star),
("tag", tag),
("search", search),
("clipboard_copy", clipboard_copy),
("upload", upload),
("download", download),
("cloud_off", cloud_off),
];
#[test]
fn every_icon_decodes() {
let ctx = egui::Context::default();
install_loaders(&ctx);
for (name, src) in ALL {
let egui::ImageSource::Bytes { uri, bytes } = src() else {
panic!("{name} is not embedded");
};
ctx.include_bytes(uri.clone(), bytes);
let poll = ctx.try_load_image(
&uri,
egui::SizeHint::Size {
width: 20,
height: 20,
maintain_aspect_ratio: true,
},
);
match poll {
Ok(egui::load::ImagePoll::Ready { .. }) => {}
Ok(egui::load::ImagePoll::Pending { .. }) => panic!("{name}: still pending"),
Err(e) => panic!("{name}: {e}"),
}
}
}
fn drawn_tints(dark: bool, mut ui: impl FnMut(&mut egui::Ui)) -> Vec<egui::Color32> {
let ctx = egui::Context::default();
install_loaders(&ctx);
crate::theme::install(
&ctx,
&crate::theme::ThemeSettings {
preference: if dark {
egui::ThemePreference::Dark
} else {
egui::ThemePreference::Light
},
..Default::default()
},
);
let mut tints = Vec::new();
for _ in 0..2 {
let mut output = ctx.run_ui(egui::RawInput::default(), &mut ui);
output.textures_delta.clear();
tints = output
.shapes
.iter()
.filter_map(|clipped| match &clipped.shape {
egui::Shape::Rect(rect) if rect.brush.is_some() => Some(rect.fill),
_ => None,
})
.collect();
}
tints
}
#[test]
fn images_follow_the_theme() {
let draw = |ui: &mut egui::Ui| {
ui.add(image(ui, settings(), 20.0));
ui.add(image_as(ui, settings(), 20.0, Tone::Text, Swatch::Accent));
};
let [dark, light] = [true, false].map(|dark| {
let theme = crate::theme::Theme::new(
dark,
crate::theme::Contrast::default(),
crate::theme::Tint::Swatch(Swatch::Blue),
);
let tints = drawn_tints(dark, draw);
assert_eq!(
tints,
[theme.text(Swatch::Neutral), theme.text(Swatch::Accent)],
"dark {dark}"
);
tints
});
assert_ne!(dark, light);
}
#[test]
fn a_button_takes_the_colour_of_its_state() {
for dark in [true, false] {
let tints = drawn_tints(dark, |ui| {
ui.add(button(settings(), 20.0));
ui.add(button(settings(), 20.0).selected(true));
ui.add_enabled(false, button(settings(), 20.0));
});
let [normal, selected, disabled] = tints[..] else {
panic!("dark {dark}: {tints:?}");
};
let (text, accent) = selected_visuals(dark);
assert_eq!(normal, text, "dark {dark}");
assert_eq!(selected, accent, "dark {dark}");
assert_ne!(normal, selected, "dark {dark}");
assert_ne!(disabled, normal, "dark {dark}");
}
}
fn selected_visuals(dark: bool) -> (egui::Color32, egui::Color32) {
let v = crate::theme::Theme::new(
dark,
crate::theme::Contrast::default(),
crate::theme::Tint::Swatch(Swatch::Blue),
)
.visuals();
(v.widgets.inactive.fg_stroke.color, v.selection.stroke.color)
}
}