egui_canvas 0.1.2

A TKinter-like canvas widget for egui.
Documentation
use crate::{predicate::ShapePredicate, widget};
#[cfg(feature = "click")]
use crate::event;
use egui::{epaint::Color32, Pos2, Vec2};
use std::{collections::btree_map::{BTreeMap, Entry}, sync::{Arc, Mutex}};
#[cfg(not(feature = "click"))]
use std::marker::PhantomData;

/// A type alias representing the closure for handling clicks
#[cfg(feature = "click")]
pub type ClickHandler<'a> = Box<dyn FnMut(event::ClickEvent) + 'a + Sync + Send>;

/// A type alias representing the inner map of shapes in the [`Canvas`]
pub type ShapeMap = BTreeMap<u32, Arc<Mutex<dyn ShapePredicate>>>;

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PositionInfo {
    pub pos: Pos2,
    pub size: Vec2
}

impl PositionInfo {
    pub fn new(pos: Pos2, size: Vec2) -> Self {
        PositionInfo { pos, size }
    }
}

/// Main global state holder struct
///
/// Lifetime `'a` is the lifetime of the click handler (or marker data if
/// the "click" feature is disabled)
#[derive(Clone)]
pub struct Canvas<'a> {
    shapes: Arc<Mutex<ShapeMap>>,
    new_id: u32,
    background_color: Option<Color32>,
    pos: Option<PositionInfo>,
    #[cfg(feature = "click")]
    click_handler: Arc<Mutex<Option<ClickHandler<'a>>>>,
    #[cfg(feature = "click")]
    has_click_handler: bool,
    #[cfg(not(feature = "click"))]
    _marker: PhantomData<&'a ()>
}

/// Constructor
impl Canvas<'_> {
    /// It is preferred to construct a [`Canvas`] using [`Canvas::default()`].
    pub fn new(shapes: ShapeMap) -> Self {
        Self {
            shapes: Arc::new(Mutex::new(shapes)),
            new_id: 0,
            background_color: None,
            pos: None,
            #[cfg(feature = "click")]
            click_handler: Arc::new(Mutex::new(None)),
            #[cfg(feature = "click")]
            has_click_handler: false,
            #[cfg(not(feature = "click"))]
            _marker: PhantomData
        }
    }
}

/// [`egui::Shape`] related methods
impl Canvas<'_> {
    /// Adds a shape into the [`Canvas`].
    ///
    /// Since all [`egui::Shape`] variants already contain position, size
    /// and color information by themselves, no need to input them as a
    /// parameter here.
    ///
    /// This method returns the inner ID of the added [`egui::Shape`]
    /// as [`u32`], or [`None`] if maximum number of [`egui::Shape`]s
    /// has been reached ([`u32::MAX`]).
    pub fn add(&mut self, pred: Arc<Mutex<dyn ShapePredicate>>) -> Option<u32> {
        let res = self.new_id;
        self.new_id = self.new_id.checked_add(1)?;
        self.shapes.lock().expect(crate::LOCK_ERR_MSG).insert(res, pred);
        Some(res)
    }

    /// Checks whether the [`Canvas`] has a [`egui::Shape`] identified
    /// by the gived ID.
    pub fn has(&self, id: u32) -> bool {
        self.shapes.lock().expect(crate::LOCK_ERR_MSG).contains_key(&id)
    }

    /// Returns the number of registered [`egui::Shape`]s in the [`Canvas`].
    pub fn shape_count(&self) -> u32 {
        self.shapes.lock().expect(crate::LOCK_ERR_MSG).len() as u32
    }

    /// Checks whether the [`Canvas`] has any [`egui::Shape`]s.
    ///
    /// This is an indirect analog to [`BTreeMap::is_empty()`].
    pub fn has_shapes(&self) -> bool {
        !self.shapes.lock().expect(crate::LOCK_ERR_MSG).is_empty()
    }

    /// Sets a [`egui::Shape`] registered in the [`Canvas`] by given ID
    /// to a different [`egui::Shape`].
    ///
    /// This method returns the [`egui::Shape`] that was previously
    /// identified by gived ID in the [`Canvas`] if there was any, otherwise
    /// it returns [`None`] (and does not set the new [`egui::Shape`]
    /// to it either).
    pub fn set(&mut self, id: u32, pred: Arc<Mutex<dyn ShapePredicate>>) -> Option<Arc<Mutex<dyn ShapePredicate>>> {
        match self.shapes.lock().expect(crate::LOCK_ERR_MSG).entry(id) {
            Entry::Occupied(mut entry) => {
                Some(entry.insert(pred))
            },
            Entry::Vacant(_) => None
        }
    }

    /// Removes a [`egui::Shape`] registered in the [`Canvas`] by given ID.
    ///
    /// This method returns the [`egui::Shape`] identified by given ID
    /// in the [`Canvas`] if there was any, otherwise it returns [`None`].
    pub fn remove(&mut self, id: u32) -> Option<Arc<Mutex<dyn ShapePredicate>>> {
        self.shapes.lock().expect(crate::LOCK_ERR_MSG).remove(&id)
    }
}

/// Attribute related methods
impl Canvas<'_> {
    /// Sets the background color of the [`Canvas`].
    ///
    /// The default background color is white.
    ///
    /// Setting backgound color to [`None`] makes the background of the
    /// [`Canvas`] transparent
    pub fn set_background(&mut self, color: Option<Color32>) {
        self.background_color = color;
    }

    /// Sets position and size information for the [`Canvas`] within the
    /// [`egui`] layout manager.
    ///
    /// This is unset by default, which makes the [`Canvas`] take up all the
    /// space available according to [`egui::Ui::available_size()`].
    pub fn set_pos(&mut self, pos: Pos2, size: Vec2) {
        self.pos = Some(PositionInfo::new(pos, size));
    }

    /// Unsets position and size information for the [`Canvas`] within the
    /// [`egui`] layout manager, which makes the [`Canvas`] take up all the
    /// space available according to [`egui::Ui::available_size()`].
    pub fn remove_pos(&mut self) {
        self.pos = None;
    }

    /// Checks whether the [`Canvas`] has a background color set, i.e.
    /// whether the background of the [`Canvas`] **is not** transparent.
    pub fn has_background(&self) -> bool {
        self.background_color.is_some()
    }

    /// Checks whether the [`Canvas`] has position and size information set
    /// within the [`egui`] layout manager, i.e. whether the [`Canvas`]
    /// **does not** take up all the space available according to
    /// [`egui::Ui::available_size()`].
    pub fn has_pos(&self) -> bool {
        self.pos.is_some()
    }
}

/// Click handler related methods
///
/// These are only included when the `"click"` feature is enabled.
#[cfg(feature = "click")]
impl<'a> Canvas<'a> {
    /// Registers a function that takes a [`ClickEvent`](event::ClickEvent)
    /// object and reacts to clicks on the [`Canvas`].
    ///
    /// See information about current limitations in the crate description.
    pub fn set_click_handler(&mut self, handler: ClickHandler<'a>) {
        *self.click_handler.lock().expect(crate::LOCK_ERR_MSG) = Some(handler);
        self.has_click_handler = true;
    }

    /// Checks whether the [`Canvas`] has a click handler registered.
    pub fn has_click_handler(&self) -> bool {
        self.has_click_handler
    }
}

/// Widget constructor
impl<'a> Canvas<'a> {
    /// Constructs a [`egui::Widget`] object from the [`Canvas`].
    ///
    /// This is what should be passed into [`egui::Ui::add()`].
    pub fn widget(&self, needless_click_sense: bool) -> widget::CanvasWidget<'a> {
        widget::CanvasWidget::new(
            self.shapes.clone(), self.background_color, self.pos, needless_click_sense,
            #[cfg(feature = "click")] self.click_handler.clone(),
            #[cfg(feature = "click")] self.has_click_handler
        )
    }
}

impl Default for Canvas<'_> {
    fn default() -> Self {
        BTreeMap::new().into()
    }
}

impl<'a> From<Canvas<'a>> for ShapeMap {
    fn from(value: Canvas<'a>) -> Self {
        value.shapes.lock().expect(crate::LOCK_ERR_MSG).clone()
    }
}

impl From<ShapeMap> for Canvas<'_> {
    fn from(value: ShapeMap) -> Self {
        let mut canvas = Self::new(value);
        canvas.set_background(Some(Color32::WHITE));
        canvas
    }
}

#[cfg(feature = "click")]
impl Drop for Canvas<'_> {
    fn drop(&mut self) {
        *self.click_handler.lock().expect(crate::LOCK_ERR_MSG) = None;
    }
}