use std::cell::{OnceCell, RefCell};
use blinc_core::Color;
use blinc_layout::element::RenderProps;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AspectRatioPreset {
Square,
Widescreen,
Traditional,
Ultrawide,
Photo,
Portrait,
Vertical,
PortraitTraditional,
}
impl AspectRatioPreset {
pub fn ratio(&self) -> f32 {
match self {
AspectRatioPreset::Square => 1.0,
AspectRatioPreset::Widescreen => 16.0 / 9.0,
AspectRatioPreset::Traditional => 4.0 / 3.0,
AspectRatioPreset::Ultrawide => 21.0 / 9.0,
AspectRatioPreset::Photo => 3.0 / 2.0,
AspectRatioPreset::Portrait => 2.0 / 3.0,
AspectRatioPreset::Vertical => 9.0 / 16.0,
AspectRatioPreset::PortraitTraditional => 3.0 / 4.0,
}
}
}
struct AspectRatioConfig {
ratio: f32,
width: Option<f32>,
height: Option<f32>,
background: Option<Color>,
corner_radius: Option<f32>,
content: Option<Box<dyn ElementBuilder>>,
}
impl Default for AspectRatioConfig {
fn default() -> Self {
Self {
ratio: 1.0, width: None,
height: None,
background: None,
corner_radius: None,
content: None,
}
}
}
struct BuiltAspectRatio {
inner: Div,
}
impl BuiltAspectRatio {
fn from_config(config: AspectRatioConfig) -> Self {
let (final_width, final_height) = match (config.width, config.height) {
(Some(w), Some(_)) => (w, w / config.ratio),
(Some(w), None) => (w, w / config.ratio),
(None, Some(h)) => (h * config.ratio, h),
(None, None) => {
let default_width = 200.0;
(default_width, default_width / config.ratio)
}
};
let mut container = div()
.class("cn-aspect-ratio")
.w(final_width)
.h(final_height)
.overflow_clip();
if let Some(bg) = config.background {
container = container.bg(bg);
}
if let Some(radius) = config.corner_radius {
container = container.rounded(radius);
}
if let Some(content) = config.content {
let content_wrapper = div()
.absolute()
.left(0.0)
.top(0.0)
.right(0.0)
.bottom(0.0)
.overflow_clip()
.child_box(content);
container = container.relative().child(content_wrapper);
}
Self { inner: container }
}
}
pub struct AspectRatio {
inner: Div,
}
impl AspectRatio {
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for AspectRatio {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct AspectRatioBuilder {
config: RefCell<AspectRatioConfig>,
built: OnceCell<AspectRatio>,
}
impl AspectRatioBuilder {
pub fn new(ratio: f32) -> Self {
Self {
config: RefCell::new(AspectRatioConfig {
ratio: ratio.max(0.01), ..Default::default()
}),
built: OnceCell::new(),
}
}
pub fn from_preset(preset: AspectRatioPreset) -> Self {
Self::new(preset.ratio())
}
fn get_or_build(&self) -> &AspectRatio {
self.built.get_or_init(|| {
let config = self.config.take();
let built = BuiltAspectRatio::from_config(config);
AspectRatio { inner: built.inner }
})
}
pub fn ratio(self, ratio: f32) -> Self {
self.config.borrow_mut().ratio = ratio.max(0.01);
self
}
pub fn w(self, width: f32) -> Self {
self.config.borrow_mut().width = Some(width);
self
}
pub fn h(self, height: f32) -> Self {
self.config.borrow_mut().height = Some(height);
self
}
pub fn bg(self, color: impl Into<Color>) -> Self {
self.config.borrow_mut().background = Some(color.into());
self
}
pub fn rounded(self, radius: f32) -> Self {
self.config.borrow_mut().corner_radius = Some(radius);
self
}
pub fn child(self, content: impl ElementBuilder + 'static) -> Self {
self.config.borrow_mut().content = Some(Box::new(content));
self
}
pub fn build_final(self) -> AspectRatio {
let config = self.config.into_inner();
let built = BuiltAspectRatio::from_config(config);
AspectRatio { inner: built.inner }
}
}
impl ElementBuilder for AspectRatioBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn aspect_ratio(ratio: f32) -> AspectRatioBuilder {
AspectRatioBuilder::new(ratio)
}
pub fn aspect_ratio_square() -> AspectRatioBuilder {
AspectRatioBuilder::from_preset(AspectRatioPreset::Square)
}
pub fn aspect_ratio_16_9() -> AspectRatioBuilder {
AspectRatioBuilder::from_preset(AspectRatioPreset::Widescreen)
}
pub fn aspect_ratio_4_3() -> AspectRatioBuilder {
AspectRatioBuilder::from_preset(AspectRatioPreset::Traditional)
}
pub fn aspect_ratio_21_9() -> AspectRatioBuilder {
AspectRatioBuilder::from_preset(AspectRatioPreset::Ultrawide)
}
pub fn aspect_ratio_9_16() -> AspectRatioBuilder {
AspectRatioBuilder::from_preset(AspectRatioPreset::Vertical)
}
#[cfg(test)]
mod tests {
use super::*;
use blinc_theme::ThemeState;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_aspect_ratio_presets() {
assert_eq!(AspectRatioPreset::Square.ratio(), 1.0);
assert!((AspectRatioPreset::Widescreen.ratio() - 16.0 / 9.0).abs() < 0.001);
assert!((AspectRatioPreset::Traditional.ratio() - 4.0 / 3.0).abs() < 0.001);
assert!((AspectRatioPreset::Vertical.ratio() - 9.0 / 16.0).abs() < 0.001);
}
#[test]
fn test_aspect_ratio_dimensions_from_width() {
init_theme();
let ratio: f32 = 16.0 / 9.0;
let width: f32 = 640.0;
let expected_height = width / ratio;
assert!((expected_height - 360.0).abs() < 0.1);
}
#[test]
fn test_aspect_ratio_dimensions_from_height() {
init_theme();
let ratio: f32 = 16.0 / 9.0;
let height: f32 = 360.0;
let expected_width = height * ratio;
assert!((expected_width - 640.0).abs() < 0.1);
}
#[test]
fn test_aspect_ratio_builder() {
init_theme();
let builder = aspect_ratio(4.0 / 3.0).w(400.0).rounded(8.0);
let config = builder.config.borrow();
assert!((config.ratio - 4.0 / 3.0).abs() < 0.001);
assert_eq!(config.width, Some(400.0));
assert_eq!(config.corner_radius, Some(8.0));
}
#[test]
fn test_square_aspect_ratio() {
init_theme();
let builder = aspect_ratio_square().w(200.0);
let config = builder.config.borrow();
assert_eq!(config.ratio, 1.0);
assert_eq!(config.width, Some(200.0));
}
}