use crate::app::Message;
use crate::theme::{self, Swatch};
use iced::widget::canvas;
pub const FONT_BYTES: &[u8] = include_bytes!("../assets/lucide-icons.ttf");
const FONT: iced::Font = iced::Font::with_name("lucide");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Icon {
Refresh,
Rotate,
Trash,
Check,
ChevronUp,
ChevronDown,
Heart,
Alert,
X,
Pencil,
Ellipsis,
PanelLeft,
ArrowUp,
Folder,
}
impl Icon {
#[cfg(test)]
const ALL: [Icon; 14] = [
Icon::Refresh,
Icon::Rotate,
Icon::Trash,
Icon::Check,
Icon::ChevronUp,
Icon::ChevronDown,
Icon::Heart,
Icon::Alert,
Icon::X,
Icon::Pencil,
Icon::Ellipsis,
Icon::PanelLeft,
Icon::ArrowUp,
Icon::Folder,
];
fn codepoint(self) -> char {
match self {
Icon::Refresh => '\u{e145}',
Icon::Rotate => '\u{e148}',
Icon::Trash => '\u{e18e}',
Icon::Check => '\u{e06c}',
Icon::ChevronUp => '\u{e070}',
Icon::ChevronDown => '\u{e06d}',
Icon::Heart => '\u{e0f2}',
Icon::Alert => '\u{e193}',
Icon::X => '\u{e1b2}',
Icon::Pencil => '\u{e1f9}',
Icon::Ellipsis => '\u{e0b6}',
Icon::PanelLeft => '\u{e12a}',
Icon::ArrowUp => '\u{e04a}',
Icon::Folder => '\u{e0d7}',
}
}
}
pub fn icon<'a, M: 'a>(which: Icon, size: f32, swatch: Swatch) -> iced::Element<'a, M> {
iced::widget::text(which.codepoint().to_string())
.font(FONT)
.size(size)
.line_height(1.0)
.style(theme::tinted(swatch))
.into()
}
struct Spinner {
angle: f32,
swatch: Swatch,
}
impl<M> canvas::Program<M> for Spinner {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
theme: &iced::Theme,
bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let width = (bounds.width / 8.0).max(1.5);
let ring = canvas::Path::new(|b| {
b.arc(canvas::path::Arc {
center: frame.center(),
radius: bounds.width.min(bounds.height) / 2.0 - width,
start_angle: iced::Radians(self.angle),
end_angle: iced::Radians(self.angle + std::f32::consts::PI * 1.5),
});
});
frame.stroke(
&ring,
canvas::Stroke::default()
.with_color(theme::palette(theme).get(self.swatch))
.with_width(width)
.with_line_cap(canvas::LineCap::Round),
);
vec![frame.into_geometry()]
}
}
pub fn spinner<'a, M: 'a>(angle: f32, size: f32, swatch: Swatch) -> iced::Element<'a, M> {
iced::widget::canvas(Spinner { angle, swatch })
.width(size)
.height(size)
.into()
}
fn tint(enabled: bool) -> Swatch {
if enabled {
Swatch::Parchment
} else {
Swatch::Slate
}
}
pub fn icon_button<'a>(
which: Icon,
tip: impl Into<String>,
on_press: Option<Message>,
) -> iced::Element<'a, Message> {
theme::tip(
iced::widget::button(icon(which, 18.0, tint(on_press.is_some())))
.on_press_maybe(on_press)
.padding([theme::SPACE_1, theme::SPACE_2])
.style(theme::button_quiet),
tip,
iced::widget::tooltip::Position::Bottom,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tile {
Pycharm,
Terminal,
}
const TILE: f32 = 20.0;
fn tile<'a, M: 'a>(which: Tile, enabled: bool) -> iced::Element<'a, M> {
let swatch = tint(enabled);
let filled = which == Tile::Pycharm;
let (label, font, size) = match which {
Tile::Pycharm => ("PC", theme::FONT_PROSE_SEMIBOLD, 9.0),
Tile::Terminal => (">_", theme::FONT_DATA, 11.0),
};
let letters = iced::widget::text(label)
.font(font)
.size(size)
.line_height(1.0)
.style(move |t: &iced::Theme| iced::widget::text::Style {
color: Some(if filled {
theme::palette(t).ink
} else {
theme::palette(t).get(swatch)
}),
});
let mut body = iced::widget::column![letters].spacing(2.0);
if filled {
body = body.push(
iced::widget::container(iced::widget::Space::new().width(7.0).height(1.5)).style(
|t: &iced::Theme| iced::widget::container::Style {
background: Some(iced::Background::Color(theme::palette(t).ink)),
..iced::widget::container::Style::default()
},
),
);
}
iced::widget::container(body)
.width(TILE)
.height(TILE)
.padding([2.0, 3.0])
.align_x(if filled {
iced::alignment::Horizontal::Left
} else {
iced::alignment::Horizontal::Center
})
.align_y(if filled {
iced::alignment::Vertical::Bottom
} else {
iced::alignment::Vertical::Center
})
.style(move |t: &iced::Theme| {
let colour = theme::palette(t).get(swatch);
iced::widget::container::Style {
background: filled.then_some(iced::Background::Color(colour)),
border: iced::Border {
color: colour,
width: 1.5,
radius: 3.0.into(),
},
..iced::widget::container::Style::default()
}
})
.into()
}
pub fn tile_button<'a>(
which: Tile,
tip: impl Into<String>,
on_press: Option<Message>,
) -> iced::Element<'a, Message> {
theme::tip(
iced::widget::button(tile(which, on_press.is_some()))
.on_press_maybe(on_press)
.padding([theme::SPACE_1, theme::SPACE_2])
.style(theme::button_quiet),
tip,
iced::widget::tooltip::Position::Bottom,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_disabled_icon_button_is_tinted_like_disabled_text() {
assert_eq!(tint(true), Swatch::Parchment);
assert_eq!(tint(false), Swatch::Slate);
}
#[test]
fn every_icon_is_a_glyph_in_the_bundled_font() {
let face = ttf_parser::Face::parse(FONT_BYTES, 0).expect("the bundled font parses");
for which in Icon::ALL {
assert!(
face.glyph_index(which.codepoint()).is_some(),
"{which:?} ({:#x}) is not in assets/lucide-icons.ttf",
which.codepoint() as u32
);
}
}
}