sequoia-view 0.1.0

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

use sequoia_core::{Length, Size};
use sequoia_layout::{LayoutCtx, LayoutStyle};
use sequoia_reactive::Signal;

use crate::any::AnyView;
use crate::runtime::{ElementId, Runtime};
use crate::view::View;
use crate::views::Unit;

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Breakpoint {
    #[default]
    Compact,

    Medium,

    Expanded,
}

impl Breakpoint {
    pub const COMPACT_MAX: f32 = 600.0;

    pub const MEDIUM_MAX: f32 = 840.0;

    pub const LARGE: f32 = 1200.0;

    pub fn of(width: f32) -> Self {
        Breakpoints::MATERIAL.of(width)
    }

    pub fn of_size(size: Size) -> Self {
        Self::of(size.width)
    }

    pub fn is_compact(self) -> bool {
        matches!(self, Self::Compact)
    }

    pub fn is_medium(self) -> bool {
        matches!(self, Self::Medium)
    }

    pub fn is_expanded(self) -> bool {
        matches!(self, Self::Expanded)
    }

    pub fn is_wide(size: Size) -> bool {
        size.width >= Self::LARGE
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Breakpoints {
    pub compact_max: f32,
    pub medium_max: f32,
}

impl Breakpoints {
    pub const MATERIAL: Self = Self {
        compact_max: Breakpoint::COMPACT_MAX,
        medium_max: Breakpoint::MEDIUM_MAX,
    };

    pub const fn new(compact_max: f32, medium_max: f32) -> Self {
        Self {
            compact_max,
            medium_max,
        }
    }

    pub fn of(self, width: f32) -> Breakpoint {
        if width < self.compact_max {
            Breakpoint::Compact
        } else if width < self.medium_max {
            Breakpoint::Medium
        } else {
            Breakpoint::Expanded
        }
    }

    pub fn of_size(self, size: Size) -> Breakpoint {
        self.of(size.width)
    }
}

impl Default for Breakpoints {
    fn default() -> Self {
        Self::MATERIAL
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ScreenOrientation {
    Portrait,
    Landscape,
    #[default]
    Square,
}

pub fn screen_orientation_of(size: Size) -> ScreenOrientation {
    if size.width > size.height {
        ScreenOrientation::Landscape
    } else if size.height > size.width {
        ScreenOrientation::Portrait
    } else {
        ScreenOrientation::Square
    }
}

pub fn orientation_of(size: Size) -> ScreenOrientation {
    screen_orientation_of(size)
}

pub struct Responsive {
    window: Signal<Size>,
    build: Rc<dyn Fn(Size) -> AnyView>,
    style: LayoutStyle,
}

impl Responsive {
    pub fn new(window: Signal<Size>, build: impl Fn(Size) -> AnyView + 'static) -> Self {
        Self {
            window,
            build: Rc::new(build),
            style: LayoutStyle::default(),
        }
    }

    pub fn window(rt: &Runtime, build: impl Fn(Size) -> AnyView + 'static) -> Self {
        Self::new(rt.window_size(), build)
    }

    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 fill_size(mut self) -> Self {
        self.style = self
            .style
            .width(Length::Fill(1.0))
            .height(Length::Fill(1.0));
        self
    }

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

impl Clone for Responsive {
    fn clone(&self) -> Self {
        Self {
            window: self.window.clone(),
            build: Rc::clone(&self.build),
            style: self.style,
        }
    }
}

impl View for Responsive {
    fn body(&self) -> impl View + 'static {
        Unit
    }

    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
        let size = self.window.get();
        Some(vec![(0, (self.build)(size))])
    }

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

pub struct WithSize {
    size: Signal<Size>,
    build: Rc<dyn Fn(Size) -> AnyView>,
    style: LayoutStyle,
}

impl WithSize {
    pub fn new(build: impl Fn(Size) -> AnyView + 'static) -> Self {
        Self::with_signal(Signal::new(Size::ZERO), build)
    }

    pub fn with_initial(initial: Size, build: impl Fn(Size) -> AnyView + 'static) -> Self {
        Self::with_signal(Signal::new(initial), build)
    }

    pub fn with_signal(size: Signal<Size>, build: impl Fn(Size) -> AnyView + 'static) -> Self {
        Self {
            size,
            build: Rc::new(build),
            style: LayoutStyle {
                width: Length::Fill(1.0),
                height: Length::Fit,
                ..LayoutStyle::default()
            },
        }
    }

    pub fn size_signal(&self) -> Signal<Size> {
        self.size.clone()
    }

    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 fill_size(mut self) -> Self {
        self.style = self
            .style
            .width(Length::Fill(1.0))
            .height(Length::Fill(1.0));
        self
    }

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

impl Clone for WithSize {
    fn clone(&self) -> Self {
        Self {
            size: self.size.clone(),
            build: Rc::clone(&self.build),
            style: self.style,
        }
    }
}

impl View for WithSize {
    fn body(&self) -> impl View + 'static {
        Unit
    }

    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
        let size = self.size.get();
        Some(vec![(0, (self.build)(size))])
    }

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

    fn arrange_override(
        &self,
        _rt: &Runtime,
        _el: ElementId,
        rect: RectAlias,
        _ctx: &mut LayoutCtx,
    ) -> bool {
        self.size.set(rect.size);
        false
    }
}

type RectAlias = sequoia_core::Rect;

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

    #[test]
    fn breakpoint_thresholds() {
        assert_eq!(Breakpoint::of(0.0), Breakpoint::Compact);
        assert_eq!(Breakpoint::of(599.9), Breakpoint::Compact);
        assert_eq!(Breakpoint::of(600.0), Breakpoint::Medium);
        assert_eq!(Breakpoint::of(839.9), Breakpoint::Medium);
        assert_eq!(Breakpoint::of(840.0), Breakpoint::Expanded);
        assert_eq!(Breakpoint::of(4000.0), Breakpoint::Expanded);
        assert!(Breakpoint::is_wide(Size::new(1200.0, 800.0)));
        assert!(!Breakpoint::is_wide(Size::new(1199.0, 800.0)));
    }

    #[test]
    fn custom_breakpoints() {
        let b = Breakpoints::new(500.0, 1000.0);
        assert_eq!(b.of(499.0), Breakpoint::Compact);
        assert_eq!(b.of(500.0), Breakpoint::Medium);
        assert_eq!(b.of(1000.0), Breakpoint::Expanded);
    }

    #[test]
    fn orientation_cases() {
        assert_eq!(
            screen_orientation_of(Size::new(800.0, 600.0)),
            ScreenOrientation::Landscape
        );
        assert_eq!(
            screen_orientation_of(Size::new(600.0, 800.0)),
            ScreenOrientation::Portrait
        );
        assert_eq!(
            screen_orientation_of(Size::new(600.0, 600.0)),
            ScreenOrientation::Square
        );
    }
}