prism 0.1.0

An experimental chart library for iced.
Documentation
use prism::chart::{
    Chart,
    series::{line_series, point, point_series},
};

use iced::{Element, Length, Task, Theme, widget::container};

fn main() -> Result<(), iced::Error> {
    iced::application(App::title, App::update, App::view)
        .theme(App::theme)
        .antialiasing(true)
        .run_with(App::new)
}

#[derive(Debug, Clone)]
enum Message {
    OnMove(Option<usize>, Option<iced::Point>),
    MouseDown(Option<usize>, Option<iced::Point>),
    MouseUp(Option<iced::Point>),
}

#[derive(Debug)]
struct App {
    handles: Vec<Handle>,
    hovered_item: Option<usize>,
    dragging: Dragging,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum ItemId {
    PointList,
}

#[derive(Debug, Default)]
enum Dragging {
    CouldStillBeClick(usize, iced::Point),
    ForSure(usize, iced::Point),
    #[default]
    None,
}

#[derive(Debug, Clone)]
struct Handle {
    coords: (f32, f32),
    style: point::Style,
}

impl Handle {
    fn new(coords: (f32, f32)) -> Self {
        Self {
            coords,
            style: point::Style::default(),
        }
    }
}

impl App {
    pub fn new() -> (Self, Task<Message>) {
        let data: Vec<_> = [(0.0, 0.0), (1.0, 1.0), (2.0, 1.0), (3.0, 0.0)]
            .into_iter()
            .map(Handle::new)
            .collect();

        (
            Self {
                handles: data,
                hovered_item: None,
                dragging: Dragging::None,
            },
            Task::none(),
        )
    }

    pub fn title(&self) -> String {
        "pliced".to_string()
    }

    pub fn update(&mut self, msg: Message) -> Task<Message> {
        match msg {
            Message::MouseDown(id, pos) => {
                let Dragging::None = self.dragging else {
                    return Task::none();
                };

                if let (Some(id), Some(pos)) = (id, pos) {
                    self.dragging = Dragging::CouldStillBeClick(id, pos);
                }
            }
            Message::OnMove(id, pos) => {
                if id.is_none() {
                    if let Some(handle) = self.hovered_item.and_then(|id| self.handles.get_mut(id))
                    {
                        handle.style = point::Style::default()
                    }
                }

                self.hovered_item = id;

                let Some(pos) = pos else {
                    return Task::none();
                };

                match self.dragging {
                    Dragging::CouldStillBeClick(id, prev_pos) => {
                        if prev_pos == pos {
                            return Task::none();
                        } else {
                            if let Some(handle) = self.handles.get_mut(id) {
                                handle.coords.0 -= prev_pos.x - pos.x;
                            }
                            self.dragging = Dragging::ForSure(id, pos);
                        }
                    }
                    Dragging::ForSure(id, prev_pos) => {
                        if let Some(handle) = self.handles.get_mut(id) {
                            handle.coords.0 -= prev_pos.x - pos.x;
                        }
                        self.dragging = Dragging::ForSure(id, pos);
                    }
                    Dragging::None => {}
                }
            }
            Message::MouseUp(pos) => {
                let Some(pos) = pos else {
                    return Task::none();
                };

                match self.dragging {
                    Dragging::CouldStillBeClick(id, _point) => {
                        if let Some(handle) = self.handles.get_mut(id) {
                            handle.style = point::Style::default();
                        }
                        self.hovered_item = None;
                        self.dragging = Dragging::None;
                    }
                    Dragging::ForSure(id, prev_pos) => {
                        if let Some(handle) = self.handles.get_mut(id) {
                            handle.coords.0 -= prev_pos.x - pos.x;
                            handle.style = point::Style::default();
                        }
                        self.dragging = Dragging::None;
                    }
                    Dragging::None => {}
                }
            }
        }

        let yellow: iced::Color = iced::Color::from_rgb8(238, 230, 0);
        let green: iced::Color = iced::Color::from_rgb8(50, 205, 50);

        match self.dragging {
            Dragging::CouldStillBeClick(id, _point) | Dragging::ForSure(id, _point) => {
                if let Some(handle) = self.handles.get_mut(id) {
                    handle.style = point::Style {
                        color: Some(green),
                        radius: 10.0,
                        ..Default::default()
                    }
                }
            }
            Dragging::None => {
                if let Some(handle) = self.hovered_item.and_then(|id| self.handles.get_mut(id)) {
                    handle.style = point::Style {
                        color: Some(yellow),
                        radius: 8.0,
                        ..Default::default()
                    }
                }
            }
        }

        Task::none()
    }

    pub fn view(&self) -> Element<'_, Message> {
        let palette = self.theme().palette();
        container(
            Chart::new()
                .width(Length::Fill)
                .height(Length::Fill)
                .x_range(-0.5..=3.5)
                .y_range(-0.5..=1.5)
                .push_series(line_series(self.handles.iter()).color(palette.primary))
                .push_series(
                    point_series(self.handles.iter())
                        .color(palette.danger)
                        .style_for_each(|_index, handle| handle.style.clone())
                        .with_id(ItemId::PointList),
                )
                .on_press(|state| {
                    let id = state.items().and_then(|l| l.first().map(|i| i.1));
                    Message::MouseDown(id, state.get_offset())
                })
                .on_move(|state| {
                    let id = state.items().and_then(|l| l.first().map(|i| i.1));
                    Message::OnMove(id, state.get_offset())
                })
                .on_release(|state| Message::MouseUp(state.get_offset())),
        )
        .into()
    }

    pub fn theme(&self) -> Theme {
        Theme::TokyoNight
    }
}

impl From<&Handle> for (f32, f32) {
    fn from(handle: &Handle) -> Self {
        handle.coords
    }
}