sequoia-elements 0.1.0

Sequoia GUI framework — built-in UI elements
Documentation
use std::cell::Cell;
use std::rc::Rc;

use sequoia_core::{Color, Edges, Length, Point};
use sequoia_input::{CursorIcon, Event, EventPhase, Key, PointerButton, PointerEvent};
use sequoia_layout::LayoutStyle;
use sequoia_reactive::{Environment, Signal};
use sequoia_style::{ContextMenuStyle, VisualState};
use sequoia_view::{
    Align, Anchor, AnyView, Column, Dismiss, EventCtx, OverlayEntry, OverlayLayer, PaintCtx,
    Placement, Row, Text, Unit, View,
};

use crate::container::Container;
use crate::popup::PopupState;

#[derive(Clone)]
enum MenuAction {
    Plain(Rc<dyn Fn()>),
    Env(Rc<dyn Fn(&Environment)>),
}

#[derive(Clone)]
struct MenuRow {
    label: String,
    action: MenuAction,
}

pub struct ContextMenu {
    child: AnyView,
    items: Vec<MenuRow>,
    popup: PopupState,

    open_at: Rc<Cell<Option<Point>>>,
    style: LayoutStyle,
    appearance: Option<ContextMenuStyle>,
    menu_background: Option<Color>,
    menu_border: Option<Color>,
    menu_radius: Option<f32>,
    menu_text_color: Option<Color>,
    menu_font_size: Option<f32>,
}

impl ContextMenu {
    pub fn new(child: impl View + 'static) -> Self {
        Self {
            child: AnyView::new(child),
            items: Vec::new(),
            popup: PopupState::new(Signal::new(false)),
            open_at: Rc::new(Cell::new(None)),
            style: LayoutStyle::default(),
            appearance: None,
            menu_background: None,
            menu_border: None,
            menu_radius: None,
            menu_text_color: None,
            menu_font_size: None,
        }
    }

    pub fn item(mut self, label: impl Into<String>, action: impl Fn() + 'static) -> Self {
        self.items.push(MenuRow {
            label: label.into(),
            action: MenuAction::Plain(Rc::new(action)),
        });
        self
    }

    pub fn item_env(
        mut self,
        label: impl Into<String>,
        action: impl Fn(&Environment) + 'static,
    ) -> Self {
        self.items.push(MenuRow {
            label: label.into(),
            action: MenuAction::Env(Rc::new(action)),
        });
        self
    }

    pub fn appearance(mut self, style: ContextMenuStyle) -> Self {
        self.appearance = Some(style);
        self
    }

    pub fn menu_background(mut self, color: Color) -> Self {
        self.menu_background = Some(color);
        self
    }

    pub fn menu_border(mut self, color: Color) -> Self {
        self.menu_border = Some(color);
        self
    }

    pub fn menu_radius(mut self, radius: f32) -> Self {
        self.menu_radius = Some(radius);
        self
    }

    pub fn menu_text_color(mut self, color: Color) -> Self {
        self.menu_text_color = Some(color);
        self
    }

    pub fn menu_font_size(mut self, size: f32) -> Self {
        self.menu_font_size = Some(size);
        self
    }

    pub fn width(mut self, width: impl Into<Length>) -> Self {
        self.style = self.style.width(width.into());
        self
    }

    pub fn height(mut self, height: impl Into<Length>) -> Self {
        self.style = self.style.height(height.into());
        self
    }

    pub fn min_width(mut self, v: f32) -> Self {
        self.style = self.style.min_width(v);
        self
    }

    pub fn max_width(mut self, v: f32) -> Self {
        self.style = self.style.max_width(v);
        self
    }

    pub fn min_height(mut self, v: f32) -> Self {
        self.style = self.style.min_height(v);
        self
    }

    pub fn max_height(mut self, v: f32) -> Self {
        self.style = self.style.max_height(v);
        self
    }

    pub fn flex(mut self, weight: f32) -> Self {
        self.style = self.style.flex(weight);
        self
    }

    pub fn expand(self) -> Self {
        self.flex(1.0)
    }

    pub fn fill_width(mut self) -> Self {
        self.style = self.style.width(Length::Fill(1.0));
        self
    }

    pub fn fill_height(mut self) -> Self {
        self.style = self.style.height(Length::Fill(1.0));
        self
    }

    pub fn fill_size(self) -> Self {
        self.fill_width().fill_height()
    }

    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
        self.style = self.style.margin(margin);
        self
    }

    pub fn margin_xy(mut self, x: f32, y: f32) -> Self {
        self.style = self.style.margin_xy(x, y);
        self
    }

    pub fn margin_x(mut self, x: f32) -> Self {
        self.style = self.style.margin_x(x);
        self
    }

    pub fn margin_y(mut self, y: f32) -> Self {
        self.style = self.style.margin_y(y);
        self
    }

    pub fn margin_left(mut self, v: f32) -> Self {
        self.style = self.style.margin_left(v);
        self
    }

    pub fn margin_right(mut self, v: f32) -> Self {
        self.style = self.style.margin_right(v);
        self
    }

    pub fn margin_top(mut self, v: f32) -> Self {
        self.style = self.style.margin_top(v);
        self
    }

    pub fn margin_bottom(mut self, v: f32) -> Self {
        self.style = self.style.margin_bottom(v);
        self
    }

    fn menu_view(&self) -> AnyView {
        let popup = self.popup.clone();
        let appearance = self.appearance.unwrap_or_default();
        let bg = self
            .menu_background
            .or(appearance.background)
            .unwrap_or(Color::hex(0x2B2B2B));
        let border = self
            .menu_border
            .or(appearance.border)
            .unwrap_or(Color::hex(0x454545));
        let radius = self.menu_radius.or(appearance.radius).unwrap_or(4.0);
        let font_size = self.menu_font_size.or(appearance.font_size).unwrap_or(12.0);
        let text_color = self.menu_text_color.or(appearance.text);
        let (pad_x, pad_y) = appearance.padding.unwrap_or((6.0, 6.0));

        let mut col = Column::new().fill_width().gap(2.0);
        for row in &self.items {
            let (action, popup) = (row.action.clone(), popup.clone());
            let mut label = Text::new(row.label.clone()).size(font_size);
            if let Some(color) = text_color {
                label = label.color(color);
            }
            let cell = Container::new()
                .fill_width()
                .background(Color::TRANSPARENT)
                .radius(3.0)
                .padding_xy(10.0, 7.0)
                .child(
                    Row::new()
                        .fill_width()
                        .align_items(Align::Center)
                        .child(label),
                );
            let cell = match action {
                MenuAction::Plain(f) => cell.on_click(move || {
                    f();
                    popup.close();
                }),
                MenuAction::Env(f) => cell.on_click_env(move |env| {
                    f(env);
                    popup.close();
                }),
            };
            col = col.child(cell);
        }
        AnyView::new(
            Container::new()
                .width(240.0)
                .background(bg)
                .border(1.0, border)
                .radius(radius)
                .padding_xy(pad_x, pad_y)
                .child(col),
        )
    }
}

impl View for ContextMenu {
    fn body(&self) -> impl View + 'static {
        self.popup.subscribe();
        Unit
    }

    fn children_override(&self) -> Option<Vec<AnyView>> {
        Some(vec![self.child.clone()])
    }

    fn layout_style(&self) -> LayoutStyle {
        self.style
    }

    fn focusable(&self) -> bool {
        true
    }

    fn has_visual_state(&self) -> bool {
        true
    }

    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
        CursorIcon::Default
    }

    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
        match event {
            Event::Pointer(PointerEvent::Down {
                button: PointerButton::Right,
                pos,
                ..
            }) => {
                if ctx.phase == EventPhase::Capture || self.items.is_empty() {
                    return;
                }

                self.open_at.set(Some(*pos));
                self.popup.open();
                ctx.request_focus();
                ctx.handled();
            }
            Event::Key(key) => {
                if ctx.phase != EventPhase::Target || !ctx.state.focused {
                    return;
                }
                if key.key == Key::ESCAPE {
                    self.popup.close_in(ctx);
                }
            }
            _ => {}
        }
    }

    fn on_unmount(&self) {
        self.popup.forget();
    }

    fn paint_self(&self, ctx: &mut PaintCtx) {
        if self.items.is_empty() {
            return;
        }

        if !self.popup.is_open() {
            self.open_at.set(None);
        }
        let menu = self.menu_view();
        let open_at = self.open_at.clone();
        self.popup.sync_show(ctx, move || {
            let anchor = open_at.get().map(Anchor::Point).unwrap_or(Anchor::Cursor);
            OverlayEntry::new(menu)
                .anchor(anchor)
                .placement(Placement::Cursor)
                .layer(OverlayLayer::Menu)
                .dismiss(Dismiss::OutsideClick)
        });
    }
}