sequoia-view 0.1.0

Sequoia GUI framework — view tree and runtime
Documentation
use std::rc::Rc;

use sequoia_core::Edges;
use sequoia_core::{Color, Length, Size};
use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
use sequoia_layout::{LayoutCtx, LayoutStyle};
use sequoia_text::{LetterSpacing, RichAlign, TextSpan, WrapMode};

use crate::view::{EventCtx, PaintCtx, View};
use crate::views::Unit;

pub struct RichText {
    spans: Box<dyn Fn() -> Vec<TextSpan>>,
    font_size: Option<f32>,
    color: Option<Color>,
    wrap_width: Option<f32>,
    wrap_mode: WrapMode,
    align: RichAlign,
    letter_spacing: f32,
    on_span_click: Option<Rc<dyn Fn(usize)>>,
    style: LayoutStyle,
}

impl RichText {
    pub fn new(spans: Vec<TextSpan>) -> Self {
        Self {
            spans: Box::new(move || spans.clone()),
            font_size: None,
            color: None,
            wrap_width: None,
            wrap_mode: WrapMode::default(),
            align: RichAlign::Left,
            letter_spacing: 0.0,
            on_span_click: None,
            style: LayoutStyle {
                width: Length::Fit,
                height: Length::Fit,
                ..LayoutStyle::default()
            },
        }
    }

    pub fn with(spans: impl Fn() -> Vec<TextSpan> + 'static) -> Self {
        Self {
            spans: Box::new(spans),
            font_size: None,
            color: None,
            wrap_width: None,
            wrap_mode: WrapMode::default(),
            align: RichAlign::Left,
            letter_spacing: 0.0,
            on_span_click: None,
            style: LayoutStyle {
                width: Length::Fit,
                height: Length::Fit,
                ..LayoutStyle::default()
            },
        }
    }

    pub fn spans(&self) -> Vec<TextSpan> {
        (self.spans)()
    }

    pub fn span(mut self, span: TextSpan) -> Self {
        let prev = std::mem::replace(&mut self.spans, Box::new(Vec::new));
        self.spans = Box::new(move || {
            let mut out = prev();
            out.push(span.clone());
            out
        });
        self
    }

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

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

    pub fn wrap_width(mut self, width: f32) -> Self {
        self.wrap_width = if width > 0.0 { Some(width) } else { None };
        self
    }

    pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
        self.wrap_mode = mode;
        self
    }

    pub fn align(mut self, align: RichAlign) -> Self {
        self.align = align;
        self
    }

    pub fn centered(mut self) -> Self {
        self.align = RichAlign::Center;
        self
    }

    pub fn right_aligned(mut self) -> Self {
        self.align = RichAlign::Right;
        self
    }

    pub fn letter_spacing(mut self, spacing: LetterSpacing) -> Self {
        self.letter_spacing = if spacing.is_finite() { spacing } else { 0.0 };
        self
    }

    pub fn on_span_click(mut self, handler: impl Fn(usize) + 'static) -> Self {
        self.on_span_click = Some(Rc::new(handler));
        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 padding(mut self, padding: impl Into<Edges>) -> Self {
        self.style = self.style.padding(padding);
        self
    }

    pub fn pad(self, padding: impl Into<Edges>) -> Self {
        self.padding(padding)
    }

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

    fn resolved(&self, theme_size: f32) -> (f32, f32) {
        (
            self.font_size.unwrap_or(theme_size),
            self.wrap_width.unwrap_or(0.0),
        )
    }
}

impl View for RichText {
    fn body(&self) -> impl View + 'static {
        let _ = (self.spans)();
        Unit
    }

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

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

    fn cursor_icon(&self, _state: sequoia_style::VisualState) -> CursorIcon {
        if self.on_span_click.is_some() {
            CursorIcon::Pointer
        } else {
            CursorIcon::Default
        }
    }

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

    fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
        let (size, wrap) = self.resolved(14.0);
        let spans = (self.spans)();
        let pad = self.style.padding;
        let text = ctx.rich_size(
            &spans,
            size,
            wrap,
            self.wrap_mode,
            self.letter_spacing,
            self.align,
        );
        Some(Size::new(
            text.width + pad.horizontal_sum(),
            text.height + pad.vertical_sum(),
        ))
    }

    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
        if ctx.phase != EventPhase::Target {
            return;
        }
        let Some(handler) = &self.on_span_click else {
            return;
        };
        if let Event::Pointer(PointerEvent::Down {
            button: PointerButton::Left,
            pos,
            ..
        }) = event
        {
            let size = self.font_size.unwrap_or_else(|| {
                ctx.environment
                    .get::<sequoia_style::Theme>()
                    .map(|t| t.font_size)
                    .unwrap_or(14.0)
            });
            let wrap = self.wrap_width.unwrap_or(0.0);
            let spans = (self.spans)();
            let pad = self.style.padding;
            let rich = ctx.shape_rich_full(
                &spans,
                size,
                wrap,
                self.wrap_mode,
                self.letter_spacing,
                self.align,
            );
            let hit = rich.hit_point(
                pos.x - ctx.rect.left() - pad.left,
                pos.y - ctx.rect.top() - pad.top,
            );
            if let Some(span) = hit.span {
                handler(span);
                ctx.handled();
            }
        }
    }

    fn paint_self(&self, ctx: &mut PaintCtx) {
        let theme = ctx.theme();
        let (size, wrap) = self.resolved(theme.font_size);
        let color = self.color.unwrap_or(theme.text);
        let spans = (self.spans)();
        if spans.iter().all(|s| s.text.is_empty()) {
            return;
        }
        let pad = self.style.padding;
        let origin = (ctx.rect.left() + pad.left, ctx.rect.top() + pad.top);
        ctx.draw_rich_spans(
            &spans,
            origin,
            size,
            wrap,
            self.wrap_mode,
            self.letter_spacing,
            self.align,
            color,
        );
    }
}