sequoia-view 0.1.0

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

use sequoia_assets::AssetRegistry;
use sequoia_core::{ImageSource, Length, Rect, Size};
use sequoia_layout::{Indents, LayoutCtx, LayoutStyle};

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

pub struct Image {
    source: ImageSource,
    style: LayoutStyle,
}

impl Image {
    pub fn file(path: impl Into<String>) -> Self {
        Self {
            source: ImageSource::File(path.into()),
            style: LayoutStyle {
                width: Length::Fill(1.0),
                height: Length::Fill(1.0),
                ..LayoutStyle::default()
            },
        }
    }

    pub fn rgba(id: u64, width: u32, height: u32, data: Vec<u8>) -> Self {
        Self {
            source: ImageSource::Rgba {
                id,
                width: width.max(1),
                height: height.max(1),
                data: Rc::from(data.into_boxed_slice()),
            },
            style: LayoutStyle {
                width: Length::Fill(1.0),
                height: Length::Fill(1.0),
                ..LayoutStyle::default()
            },
        }
    }

    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<sequoia_core::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
    }

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

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

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

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

    pub fn clear_indents(mut self) -> Self {
        self.style = self.style.clear_indents();
        self
    }

    pub fn indent(mut self, indents: Indents) -> Self {
        self.style = self.style.indent(indents);
        self
    }

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

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

    pub fn indent_axes(mut self, horizontal: f32, vertical: f32) -> Self {
        self.style = self.style.indent_axes(horizontal, vertical);
        self
    }

    pub fn dynamic(id: u64, width: u32, height: u32, data: Vec<u8>, version: u64) -> Self {
        Self {
            source: ImageSource::Dynamic {
                id,
                width: width.max(1),
                height: height.max(1),
                data: Rc::from(data.into_boxed_slice()),
                version,
            },
            style: LayoutStyle {
                width: Length::Fill(1.0),
                height: Length::Fill(1.0),
                ..LayoutStyle::default()
            },
        }
    }

    #[allow(unreachable_patterns)]
    fn aspect(&self, ctx: &PaintCtx) -> Option<(f32, f32)> {
        match &self.source {
            ImageSource::Rgba { width, height, .. }
            | ImageSource::Dynamic { width, height, .. } => {
                if *width > 0 && *height > 0 {
                    Some((*width as f32, *height as f32))
                } else {
                    None
                }
            }
            ImageSource::File(path) => ctx
                .environment
                .get::<AssetRegistry>()
                .and_then(|assets| assets.photo_size(path).ok())
                .filter(|(w, h)| *w > 0 && *h > 0)
                .map(|(w, h)| (w as f32, h as f32)),

            _ => None,
        }
    }
}

fn contain_fit(outer: Rect, aspect: (f32, f32)) -> Rect {
    let (aw, ah) = aspect;
    if aw <= 0.0 || ah <= 0.0 || outer.size.width <= 0.0 || outer.size.height <= 0.0 {
        return outer;
    }
    let scale = (outer.size.width / aw).min(outer.size.height / ah);
    let w = aw * scale;
    let h = ah * scale;
    Rect::from_xywh(
        outer.left() + (outer.size.width - w) * 0.5,
        outer.top() + (outer.size.height - h) * 0.5,
        w,
        h,
    )
}

impl View for Image {
    fn body(&self) -> impl View + 'static {
        Unit
    }
    fn is_leaf(&self) -> bool {
        true
    }
    fn layout_style(&self) -> LayoutStyle {
        self.style
    }

    #[allow(unreachable_patterns)]
    fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
        match &self.source {
            ImageSource::Rgba { width, height, .. }
            | ImageSource::Dynamic { width, height, .. } => {
                Some(Size::new(*width as f32, *height as f32))
            }
            ImageSource::File(_) => None,

            _ => None,
        }
    }
    fn paint_self(&self, ctx: &mut PaintCtx) {
        let rect = ctx.rect;
        if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
            return;
        }
        let dst = self
            .aspect(ctx)
            .map(|a| contain_fit(rect, a))
            .unwrap_or(rect);
        if dst.is_empty() {
            return;
        }
        ctx.image(dst, [0.0, 0.0, 1.0, 1.0], self.source.clone());
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn contain_fit_centers_without_distortion() {
        let outer = Rect::from_xywh(0.0, 0.0, 100.0, 100.0);

        let r = contain_fit(outer, (40.0, 30.0));
        assert_eq!((r.size.width, r.size.height), (100.0, 75.0));
        assert_eq!((r.left(), r.top()), (0.0, 12.5));

        let r = contain_fit(outer, (30.0, 40.0));
        assert_eq!((r.size.width, r.size.height), (75.0, 100.0));
        assert_eq!((r.left(), r.top()), (12.5, 0.0));
    }

    #[test]
    fn contain_fit_degenerate_falls_back_to_outer() {
        let outer = Rect::from_xywh(1.0, 2.0, 10.0, 10.0);
        assert_eq!(contain_fit(outer, (0.0, 5.0)), outer);
        assert_eq!(contain_fit(Rect::ZERO, (4.0, 3.0)), Rect::ZERO);
    }
}