#[cfg(feature = "interaction")]
use crate::region::Region;
use crate::style::Style;
use crate::widget_state::{RenderState, WidgetId, WidgetStates};
use core::fmt::Debug;
use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::geometry::Dimensions;
use embedded_graphics::pixelcolor::PixelColor;
#[cfg(feature = "interaction")]
use embedded_graphics::prelude::Point;
use embedded_graphics::primitives::Rectangle;
use embedded_graphics::{Drawable, Pixel};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum GuiError {
DrawError,
InvalidWidgetId,
InvalidAnimId,
ModalActive,
}
pub type GuiResult<T> = Result<T, GuiError>;
#[cfg(feature = "interaction")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[non_exhaustive]
pub enum Interaction {
Pressed(Point),
Drag(Point),
Release(Point),
Clicked(Point),
#[default]
None,
}
#[cfg(feature = "interaction")]
impl Interaction {
fn get_point(&self) -> Option<Point> {
match self {
Interaction::Pressed(p) => Some(*p),
Interaction::Drag(p) => Some(*p),
Interaction::Release(p) => Some(*p),
Interaction::Clicked(p) => Some(*p),
Interaction::None => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Response {
Idle,
Clicked,
Pressed,
Released,
Focused,
Defocused,
Selected,
Trigger,
Edited,
ValueChanged,
Hovered,
Scrolled,
Disabled,
User(u8),
Error(GuiError),
}
impl Response {
pub const fn is_idle(&self) -> bool {
matches!(self, Response::Idle)
}
pub const fn is_clicked(&self) -> bool {
matches!(self, Response::Clicked)
}
pub const fn is_pressed(&self) -> bool {
matches!(self, Response::Pressed)
}
pub const fn is_released(&self) -> bool {
matches!(self, Response::Released)
}
pub const fn is_focused(&self) -> bool {
matches!(self, Response::Focused)
}
pub const fn is_defocused(&self) -> bool {
matches!(self, Response::Defocused)
}
pub const fn is_selected(&self) -> bool {
matches!(self, Response::Selected)
}
pub const fn is_trigger(&self) -> bool {
matches!(self, Response::Trigger)
}
pub const fn is_edited(&self) -> bool {
matches!(self, Response::Edited)
}
pub const fn is_value_changed(&self) -> bool {
matches!(self, Response::ValueChanged)
}
pub const fn is_hovered(&self) -> bool {
matches!(self, Response::Hovered)
}
pub const fn is_scrolled(&self) -> bool {
matches!(self, Response::Scrolled)
}
pub const fn is_disabled(&self) -> bool {
matches!(self, Response::Disabled)
}
pub const fn is_user(&self) -> bool {
matches!(self, Response::User(_))
}
pub fn user_code(&self) -> Option<u8> {
match self {
Response::User(code) => Some(*code),
_ => None,
}
}
pub fn error(&self) -> Option<GuiError> {
match self {
Response::Error(e) => Some(*e),
_ => None,
}
}
}
impl Response {
pub fn from_error(error: GuiError) -> Response {
Response::Error(error)
}
pub fn from_change(change: bool) -> Self {
if change {
Response::ValueChanged
} else {
Response::Idle
}
}
}
#[cfg(feature = "interaction")]
impl From<Interaction> for Response {
fn from(interaction: Interaction) -> Self {
match interaction {
Interaction::None => Response::Idle,
Interaction::Pressed(_) | Interaction::Drag(_) => Response::Pressed,
Interaction::Release(_) => Response::Released,
Interaction::Clicked(_) => Response::Clicked,
}
}
}
pub trait Widget<DRAW: DrawTarget<Color = COL>, COL: PixelColor> {
fn draw(&mut self, ui: &mut Ui<DRAW, COL>) -> GuiResult<Response>;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HorizontalAlign {
Left,
Center,
Right,
Justify,
}
#[derive(Clone, Copy, Debug)]
pub enum VerticalAlign {
Top,
Center,
Bottom,
}
#[derive(Clone, Copy, Debug)]
pub struct Align(pub HorizontalAlign, pub VerticalAlign);
impl Default for Align {
fn default() -> Self {
Align(HorizontalAlign::Left, VerticalAlign::Top)
}
}
struct Painter<'a, COL: PixelColor, DRAW: DrawTarget<Color = COL>> {
target: &'a mut DRAW,
}
impl<'a, COL: PixelColor, DRAW: DrawTarget<Color = COL>> Painter<'a, COL, DRAW> {
fn new(target: &'a mut DRAW) -> Self {
Self { target }
}
fn draw(&mut self, item: &impl Drawable<Color = COL>) -> GuiResult<()> {
item.draw(self.target).map_err(|_| GuiError::DrawError)?;
Ok(())
}
}
impl<COL: PixelColor, DRAW: DrawTarget<Color = COL, Error = ERR>, ERR> Dimensions
for Painter<'_, COL, DRAW>
{
fn bounding_box(&self) -> Rectangle {
self.target.bounding_box()
}
}
impl<COL: PixelColor, DRAW: DrawTarget<Color = COL, Error = ERR>, ERR> DrawTarget
for Painter<'_, COL, DRAW>
{
type Color = COL;
type Error = ERR;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
self.target.draw_iter(pixels)
}
}
pub struct Ui<'a, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
bounds: Rectangle,
painter: Painter<'a, COL, DRAW>,
widget_states: &'a WidgetStates<'a>,
style: &'a Style<COL>,
cleared: bool,
#[cfg(feature = "interaction")]
interact: Interaction,
#[cfg(feature = "focus")]
focus: Option<crate::focus::Focus<'a>>,
#[cfg(feature = "debug-color")]
debug_color: Option<COL>,
}
impl<DRAW, COL> Ui<'_, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub const fn get_screen_width(&self) -> u32 {
self.bounds.size.width
}
pub const fn get_screen_height(&self) -> u32 {
self.bounds.size.height
}
pub const fn get_screen_bounds(&self) -> Rectangle {
self.bounds
}
}
impl<'a, COL, DRAW> Ui<'a, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub fn new(
drawable: &'a mut DRAW,
bounds: Rectangle,
widget_states: &'a WidgetStates<'a>,
style: &'a Style<COL>,
) -> Self {
Self {
bounds,
painter: Painter::new(drawable),
widget_states,
style,
cleared: false,
#[cfg(feature = "interaction")]
interact: Interaction::None,
#[cfg(feature = "focus")]
focus: None,
#[cfg(feature = "debug-color")]
debug_color: None,
}
}
pub fn new_fullscreen(
drawable: &'a mut DRAW,
widget_states: &'a WidgetStates<'a>,
style: &'a Style<COL>,
) -> Self {
let bounds = drawable.bounding_box();
Ui::new(drawable, bounds, widget_states, style)
}
pub fn add(&mut self, mut widget: impl Widget<DRAW, COL>) -> Response {
widget.draw(self).unwrap_or_else(Response::from_error)
}
pub fn lazy_draw<F, ID: WidgetId>(&mut self, id: ID, f: F) -> Response
where
F: FnOnce(&mut Ui<DRAW, COL>) -> Response,
{
if self.widget_states.should_redraw(id) {
f(self)
} else {
Response::Idle
}
}
pub fn style(&self) -> &Style<COL> {
self.style
}
#[cfg(feature = "interaction")]
pub fn interact(&mut self, interaction: Interaction) {
self.interact = interaction;
}
#[cfg(feature = "interaction")]
pub fn check_interact<ID: WidgetId>(&mut self, region: &Region<ID>) -> Interaction {
self.widget_states.mark_as_interact(region.id());
#[cfg(feature = "popup")]
if self.is_modal_active() {
return Interaction::None;
}
let area = ®ion.rectangle();
if self
.interact
.get_point()
.map(|pt| area.contains(pt))
.unwrap_or(false)
{
self.interact
} else {
Interaction::None
}
}
}
impl<COL, DRAW> Ui<'_, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub const fn cleared(&self) -> bool {
self.cleared
}
pub fn clear_area(&mut self, area: &Rectangle) -> GuiResult<()> {
#[cfg(not(feature = "debug-color"))]
if self.cleared {
return Ok(());
}
self.clear_area_raw(area, self.style.background_color)?;
#[cfg(feature = "debug-color")]
self.draw_widget_bound_box(area);
Ok(())
}
pub fn clear_area_raw(&mut self, area: &Rectangle, color: COL) -> GuiResult<()> {
#[cfg(not(feature = "fill-rect"))]
{
use embedded_graphics::primitives::{PrimitiveStyle, StyledDrawable};
area.draw_styled(&PrimitiveStyle::with_fill(color), self.painter.target)
.map_err(|_| GuiError::DrawError)
}
#[cfg(feature = "fill-rect")]
{
crate::fill_rect::fill_with_color(area, color);
Ok(())
}
}
pub fn clear_background(&mut self) -> GuiResult<()> {
self.cleared = true;
self.clear_area_raw(&self.bounds.clone(), self.style.background_color)
}
}
impl<'a, COL, DRAW> Ui<'a, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub fn draw(&mut self, item: &impl Drawable<Color = COL>) -> GuiResult<()> {
self.painter.draw(item)
}
}
impl<'a, COL, DRAW> Ui<'a, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub fn get_widget_state<ID: WidgetId>(&self, widget_id: ID) -> GuiResult<&RenderState> {
self.widget_states.get_state(widget_id)
}
#[cfg(feature = "animation")]
pub fn take_anim_status(&self, anim_id: crate::prelude::AnimId) -> GuiResult<Option<i32>> {
self.widget_states.take_anim_status(anim_id)
}
#[cfg(feature = "animation")]
pub fn get_anim_status(&self, anim_id: crate::prelude::AnimId) -> GuiResult<Option<i32>> {
self.widget_states.get_anim_status(anim_id)
}
pub fn force_redraw<ID: WidgetId>(&self, widget_id: ID) {
self.widget_states.force_redraw(widget_id);
}
pub fn force_redraw_all(&self) {
self.widget_states.force_redraw_all();
}
}
#[cfg(feature = "focus")]
impl<'a, COL, DRAW> Ui<'a, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub fn set_focus_state<const N: usize>(
&mut self,
focus_state: &'a mut crate::focus::FocusState<N>,
) {
self.focus = Some(crate::focus::Focus::new(focus_state));
}
pub fn check_focused<ID: WidgetId>(&mut self, id: ID) -> crate::focus::Focused {
use crate::focus::Focused;
#[cfg(feature = "popup")]
if self.is_modal_active() {
return Focused::No;
}
if let Some(focus) = self.focus.as_mut() {
self.widget_states.mark_as_interact(id);
if let Some(trigger_id) = focus.tracker.trigger
&& trigger_id == id.id() as u16
{
focus.tracker.trigger = None;
return Focused::Trigger;
} else if focus.register_focus(id.id()) && focus.is_focused(id.id()) {
return Focused::Yes;
}
}
Focused::No
}
}
#[cfg(feature = "popup")]
impl<'a, COL, DRAW> Ui<'a, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub const fn is_modal_active(&self) -> bool {
self.widget_states.is_modal_active()
}
pub fn set_modal_active(&self, active: bool) {
self.widget_states.set_modal_active(active);
}
}
#[cfg(feature = "debug-color")]
impl<COL, DRAW> Ui<'_, DRAW, COL>
where
DRAW: DrawTarget<Color = COL>,
COL: PixelColor,
{
pub fn draw_bounds_debug(&mut self, color: COL) -> GuiResult<()> {
use embedded_graphics::primitives::{PrimitiveStyleBuilder, StyledDrawable};
let bounds = self.bounds;
bounds
.draw_styled(
&PrimitiveStyleBuilder::new()
.stroke_color(color)
.stroke_width(1)
.build(),
&mut self.painter,
)
.map_err(|_| GuiError::DrawError)
}
pub fn draw_widget_bounds_debug(&mut self, color: COL) {
self.debug_color = Some(color);
}
pub fn draw_widget_bound_box(&mut self, area: &Rectangle) {
if let Some(debug_color) = self.debug_color {
use embedded_graphics::primitives::{PrimitiveStyleBuilder, StyledDrawable};
area.draw_styled(
&PrimitiveStyleBuilder::new()
.stroke_color(debug_color)
.stroke_width(1)
.build(),
&mut self.painter,
)
.ok();
}
}
}