use embedded_graphics::{
Drawable,
mono_font::{MonoTextStyleBuilder, ascii::FONT_6X10},
pixelcolor::Rgb565,
prelude::*,
primitives::{PrimitiveStyle, Rectangle},
text::{Alignment, Text},
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Insets {
pub left: u32,
pub top: u32,
pub right: u32,
pub bottom: u32,
}
impl Insets {
#[must_use]
pub const fn symmetric(horizontal: u32, vertical: u32) -> Self {
Self {
left: horizontal,
top: vertical,
right: horizontal,
bottom: vertical,
}
}
#[must_use]
pub fn apply(self, area: Rectangle) -> Rectangle {
let width = area
.size
.width
.saturating_sub(self.left.saturating_add(self.right));
let height = area
.size
.height
.saturating_sub(self.top.saturating_add(self.bottom));
Rectangle::new(
area.top_left + Point::new(self.left as i32, self.top as i32),
Size::new(width, height),
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextAlign {
Left,
Center,
Right,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LabelStyle {
pub color: Rgb565,
pub background: Option<Rgb565>,
pub align: TextAlign,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextBlockStyle {
pub color: Rgb565,
pub background: Option<Rgb565>,
pub line_height: u32,
}
impl TextBlockStyle {
#[must_use]
pub const fn new(color: Rgb565) -> Self {
Self {
color,
background: None,
line_height: 12,
}
}
#[must_use]
pub const fn with_background(mut self, background: Rgb565) -> Self {
self.background = Some(background);
self
}
}
impl LabelStyle {
#[must_use]
pub const fn centered(color: Rgb565) -> Self {
Self {
color,
background: None,
align: TextAlign::Center,
}
}
#[must_use]
pub const fn with_background(mut self, background: Rgb565) -> Self {
self.background = Some(background);
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScreenLayout {
bounds: Rectangle,
}
impl ScreenLayout {
#[must_use]
pub fn new(size: Size) -> Self {
Self {
bounds: Rectangle::new(Point::zero(), size),
}
}
#[must_use]
pub const fn bounds(&self) -> Rectangle {
self.bounds
}
#[must_use]
pub fn content(&self, insets: Insets) -> Rectangle {
insets.apply(self.bounds)
}
#[must_use]
pub fn row(&self, y: i32, height: u32, insets: Insets) -> Rectangle {
let content = self.content(insets);
Rectangle::new(
Point::new(content.top_left.x, y),
Size::new(content.size.width, height),
)
}
}
pub trait View<D>
where
D: DrawTarget<Color = Rgb565>,
{
fn render(&mut self, target: &mut D) -> Result<(), D::Error>;
}
pub fn draw_label<D>(
target: &mut D,
area: Rectangle,
text: &str,
style: LabelStyle,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
{
if let Some(background) = style.background {
area.into_styled(PrimitiveStyle::with_fill(background))
.draw(target)?;
}
let (x, alignment) = match style.align {
TextAlign::Left => (area.top_left.x, Alignment::Left),
TextAlign::Center => (
area.top_left.x + (area.size.width / 2) as i32,
Alignment::Center,
),
TextAlign::Right => (area.top_left.x + area.size.width as i32, Alignment::Right),
};
let y = area.top_left.y + (area.size.height / 2) as i32 + 4;
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_6X10)
.text_color(style.color)
.build();
Text::with_alignment(text, Point::new(x, y), text_style, alignment)
.draw(target)
.map(|_| ())
}
pub fn draw_progress_bar<D>(
target: &mut D,
area: Rectangle,
value: u8,
foreground: Rgb565,
background: Rgb565,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
{
area.into_styled(PrimitiveStyle::with_fill(background))
.draw(target)?;
let clamped = value.min(100);
let filled_width = area.size.width.saturating_mul(u32::from(clamped)) / 100;
if filled_width == 0 {
return Ok(());
}
Rectangle::new(area.top_left, Size::new(filled_width, area.size.height))
.into_styled(PrimitiveStyle::with_fill(foreground))
.draw(target)
}
pub fn draw_wrapped_text<D>(
target: &mut D,
area: Rectangle,
text: &str,
style: TextBlockStyle,
) -> Result<usize, D::Error>
where
D: DrawTarget<Color = Rgb565>,
{
if let Some(background) = style.background {
area.into_styled(PrimitiveStyle::with_fill(background))
.draw(target)?;
}
let max_chars = (area.size.width / 6).max(1) as usize;
let max_lines = (area.size.height / style.line_height).max(1) as usize;
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_6X10)
.text_color(style.color)
.build();
let mut lines_drawn = 0usize;
let mut remaining = text.trim();
while !remaining.is_empty() && lines_drawn < max_lines {
let (line, rest) = split_line(remaining, max_chars);
let baseline = area.top_left.y + 10 + (lines_drawn as i32 * style.line_height as i32);
Text::new(line, Point::new(area.top_left.x, baseline), text_style).draw(target)?;
lines_drawn += 1;
remaining = rest.trim_start();
}
Ok(lines_drawn)
}
fn split_line(text: &str, max_chars: usize) -> (&str, &str) {
if text.chars().count() <= max_chars {
return (text, "");
}
let mut split_byte = 0;
let mut last_space = None;
for (char_count, (byte_index, ch)) in text.char_indices().enumerate() {
if char_count == max_chars {
break;
}
split_byte = byte_index + ch.len_utf8();
if ch.is_whitespace() {
last_space = Some(byte_index);
}
}
let split_at = last_space.filter(|space| *space > 0).unwrap_or(split_byte);
(&text[..split_at], &text[split_at..])
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Easing {
Linear,
SmoothStep,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Transition {
from: i32,
to: i32,
frames: u16,
current: u16,
easing: Easing,
}
impl Transition {
#[must_use]
pub const fn new(from: i32, to: i32, frames: u16, easing: Easing) -> Self {
Self {
from,
to,
frames,
current: 0,
easing,
}
}
#[must_use]
pub fn step(&mut self) -> i32 {
if self.current < self.frames {
self.current += 1;
}
self.value()
}
#[must_use]
pub fn value(&self) -> i32 {
if self.frames == 0 {
return self.to;
}
let progress = self.current.min(self.frames) as f32 / self.frames as f32;
let t = match self.easing {
Easing::Linear => progress,
Easing::SmoothStep => progress * progress * (3.0 - 2.0 * progress),
};
self.from + ((self.to - self.from) as f32 * t) as i32
}
#[must_use]
pub const fn is_finished(&self) -> bool {
self.current >= self.frames
}
}