ultron-web 0.3.0

Ultron editor variant exposed as a web application
Documentation
use sauron::dom::Callback;
use sauron::{html::attributes::*, html::events::*, html::*, *};
use ultron_core::nalgebra::Point2;

#[derive(Debug, Clone)]
pub enum Msg {
    SelectAction(MenuAction),
    ShowAt(Point2<i32>),
}

pub struct Menu<XMSG> {
    listeners: Vec<Callback<MenuAction, XMSG>>,
    position: Option<Point2<i32>>,
}

impl<XMSG> Default for Menu<XMSG> {
    fn default() -> Self {
        Self {
            listeners: vec![],
            position: None,
        }
    }
}

#[derive(Clone, Debug, Copy)]
pub enum MenuAction {
    Undo,
    Redo,
    Cut,
    Copy,
    Paste,
    Delete,
    SelectAll,
}

impl<XMSG> Menu<XMSG> {
    pub fn new() -> Self {
        Self {
            listeners: vec![],
            position: None,
        }
    }
}

impl<XMSG> Menu<XMSG> {
    pub(crate) fn on_activate<F>(mut self, f: F) -> Self
    where
        F: Fn(MenuAction) -> XMSG + 'static,
    {
        self.listeners.push(Callback::from(f));
        self
    }
}

impl<XMSG> Component<Msg, XMSG> for Menu<XMSG> {
    fn update(&mut self, msg: Msg) -> Effects<Msg, XMSG> {
        match msg {
            Msg::SelectAction(menu_action) => {
                let xmsgs: Vec<XMSG> = self
                    .listeners
                    .iter()
                    .map(|listener| listener.emit(menu_action))
                    .collect();
                Effects::with_external(xmsgs)
            }
            Msg::ShowAt(point) => {
                self.position = Some(point);
                Effects::none()
            }
        }
    }

    fn view(&self) -> Node<Msg> {
        let pos = self.position.unwrap_or(Point2::new(0, 0));
        div(
            [
                class("context_menu"),
                style! {
                   position: "absolute",
                   top: px(pos.y),
                   left: px(pos.x),
                },
            ],
            [details(
                [open(true)],
                [
                    html::summary([], [text(" ")]),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::Undo))],
                        [text("Undo")],
                    ),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::Redo))],
                        [text("Redo")],
                    ),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::Cut))],
                        [text("Cut")],
                    ),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::Copy))],
                        [text("Copy")],
                    ),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::Paste))],
                        [text("Paste")],
                    ),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::Delete))],
                        [text("Delete")],
                    ),
                    li(
                        [on_click(|_| Msg::SelectAction(MenuAction::SelectAll))],
                        [text("Select All")],
                    ),
                ],
            )],
        )
    }

    fn style(&self) -> String {
        jss! {
            ".context_menu": {
                background_color: "#eee",
                display: "flex",
                flex_direction: "row",
                user_select: "none",
                "-webkit-user-select": "none",
            },

            ".context_menu details": {
                position: "absolute",
                display: "flex",
                width: px(120),
                flex_direction: "column",
                justify_content: "center",
                align_content: "center",
                border: format!("{} solid #ccc",px(1)),
                cursor: "default",
            },

            ".context_menu details[open]": {
                background_color: "#eee",
                border_bottom: 0,
            },

            ".context_menu details summary": {
                list_style: "none",
                outline: "none",
                width: px(120),
                padding: px([5, 5]),
            },

            ".context_menu details summary::-webkit-details-marker": {
                display: "none",
            },

            ".context_menu details[open] summary": {
                border_bottom: format!("{} solid #ccc", px(1)),
            },

            ".context_menu details li": {
                list_style: "none",
                padding: px([5, 5]),
                border_bottom: format!("{} solid #ddd", px(1)),
            },

            ".context_menu details li:hover": {
                background_color: "#ddd",
            },
        }
    }
}