#[cfg(feature = "widgets")]
mod inner {
pub use presentar::layout;
pub use presentar::widgets;
pub use presentar::yaml;
pub use presentar::browser::{BrowserRouter, RouteMatch, RouteMatcher};
pub use presentar::browser::{Cell, CellGraph, CellId, CellOutput, NotebookRuntime};
}
#[cfg(feature = "widgets")]
pub use inner::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
#[must_use]
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
#[must_use]
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
#[must_use]
pub fn to_f32(self) -> [f32; 4] {
[
f32::from(self.r) / 255.0,
f32::from(self.g) / 255.0,
f32::from(self.b) / 255.0,
f32::from(self.a) / 255.0,
]
}
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const RED: Self = Self::rgb(255, 0, 0);
pub const GREEN: Self = Self::rgb(0, 255, 0);
pub const BLUE: Self = Self::rgb(0, 0, 255);
pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
}
impl Default for Color {
fn default() -> Self {
Self::BLACK
}
}
pub fn parse_hex_color(hex: &str) -> Result<Color, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 && hex.len() != 8 {
return Err(format!("Invalid hex color length: {}", hex.len()));
}
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?;
let a = if hex.len() == 8 {
u8::from_str_radix(&hex[6..8], 16).map_err(|e| e.to_string())?
} else {
255
};
Ok(Color::rgba(r, g, b, a))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Start,
Center,
End,
Stretch,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Widget {
Column {
children: Vec<Widget>,
alignment: Alignment,
spacing: f32,
},
Row {
children: Vec<Widget>,
alignment: Alignment,
spacing: f32,
},
Text {
content: String,
font_size: f32,
color: Color,
},
Button {
label: String,
on_click: String,
color: Color,
},
Input { placeholder: String, value: String },
Container {
child: Box<Widget>,
padding: f32,
background: Option<Color>,
},
}
impl Widget {
pub fn text(content: &str) -> Self {
Self::Text {
content: content.to_string(),
font_size: 16.0,
color: Color::BLACK,
}
}
pub fn button(label: &str) -> Self {
Self::Button {
label: label.to_string(),
on_click: String::new(),
color: Color::rgb(66, 133, 244),
}
}
pub fn column(children: Vec<Widget>) -> Self {
Self::Column {
children,
alignment: Alignment::Start,
spacing: 8.0,
}
}
pub fn row(children: Vec<Widget>) -> Self {
Self::Row {
children,
alignment: Alignment::Start,
spacing: 8.0,
}
}
pub fn widget_count(&self) -> usize {
match self {
Widget::Column { children, .. } | Widget::Row { children, .. } => {
1 + children.iter().map(|c| c.widget_count()).sum::<usize>()
}
Widget::Container { child, .. } => 1 + child.widget_count(),
_ => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderTarget {
Wasm,
Native,
Ssr,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_rgb() {
let c = Color::rgb(255, 128, 0);
assert_eq!(c.r, 255);
assert_eq!(c.g, 128);
assert_eq!(c.b, 0);
assert_eq!(c.a, 255);
}
#[test]
fn test_color_rgba() {
let c = Color::rgba(100, 150, 200, 128);
assert_eq!(c.r, 100);
assert_eq!(c.g, 150);
assert_eq!(c.b, 200);
assert_eq!(c.a, 128);
}
#[test]
fn test_color_to_f32() {
let c = Color::rgb(255, 0, 128);
let f = c.to_f32();
assert!((f[0] - 1.0).abs() < 1e-5);
assert!((f[1] - 0.0).abs() < 1e-5);
assert!((f[2] - 0.501_960_8).abs() < 1e-5);
assert!((f[3] - 1.0).abs() < 1e-5);
}
#[test]
fn test_parse_hex_color_6_digit() {
let c = parse_hex_color("#FF8000").unwrap();
assert_eq!(c.r, 255);
assert_eq!(c.g, 128);
assert_eq!(c.b, 0);
assert_eq!(c.a, 255);
}
#[test]
fn test_parse_hex_color_8_digit() {
let c = parse_hex_color("FF800080").unwrap();
assert_eq!(c.r, 255);
assert_eq!(c.g, 128);
assert_eq!(c.b, 0);
assert_eq!(c.a, 128);
}
#[test]
fn test_parse_hex_color_invalid() {
assert!(parse_hex_color("FFF").is_err());
assert!(parse_hex_color("GGGGGG").is_err());
}
#[test]
fn test_color_constants() {
assert_eq!(Color::WHITE.r, 255);
assert_eq!(Color::BLACK.r, 0);
assert_eq!(Color::RED.r, 255);
assert_eq!(Color::RED.g, 0);
assert_eq!(Color::TRANSPARENT.a, 0);
}
#[test]
fn test_widget_text() {
let w = Widget::text("Hello");
if let Widget::Text {
content, font_size, ..
} = &w
{
assert_eq!(content, "Hello");
assert!((font_size - 16.0).abs() < 1e-5);
} else {
panic!("Expected Text widget");
}
}
#[test]
fn test_widget_button() {
let w = Widget::button("Click me");
if let Widget::Button { label, .. } = &w {
assert_eq!(label, "Click me");
} else {
panic!("Expected Button widget");
}
}
#[test]
fn test_widget_column() {
let col = Widget::column(vec![Widget::text("A"), Widget::text("B")]);
if let Widget::Column {
children, spacing, ..
} = &col
{
assert_eq!(children.len(), 2);
assert!((spacing - 8.0).abs() < 1e-5);
} else {
panic!("Expected Column widget");
}
}
#[test]
fn test_widget_row() {
let row = Widget::row(vec![Widget::button("OK"), Widget::button("Cancel")]);
if let Widget::Row { children, .. } = &row {
assert_eq!(children.len(), 2);
} else {
panic!("Expected Row widget");
}
}
#[test]
fn test_widget_count() {
let tree = Widget::column(vec![
Widget::text("Title"),
Widget::row(vec![Widget::button("A"), Widget::button("B")]),
]);
assert_eq!(tree.widget_count(), 5); }
#[test]
fn test_alignment_variants() {
assert_ne!(Alignment::Start, Alignment::Center);
assert_ne!(Alignment::Center, Alignment::End);
assert_ne!(Alignment::End, Alignment::Stretch);
}
#[test]
fn test_render_target_variants() {
assert_ne!(RenderTarget::Wasm, RenderTarget::Native);
assert_ne!(RenderTarget::Native, RenderTarget::Ssr);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn prop_color_to_f32_bounded(r in 0u8..=255, g in 0u8..=255, b in 0u8..=255, a in 0u8..=255) {
let c = Color::rgba(r, g, b, a);
let f = c.to_f32();
prop_assert!(f[0] >= 0.0 && f[0] <= 1.0);
prop_assert!(f[1] >= 0.0 && f[1] <= 1.0);
prop_assert!(f[2] >= 0.0 && f[2] <= 1.0);
prop_assert!(f[3] >= 0.0 && f[3] <= 1.0);
}
#[test]
fn prop_hex_roundtrip(r in 0u8..=255, g in 0u8..=255, b in 0u8..=255) {
let hex = format!("{r:02X}{g:02X}{b:02X}");
let c = parse_hex_color(&hex).unwrap();
prop_assert_eq!(c.r, r);
prop_assert_eq!(c.g, g);
prop_assert_eq!(c.b, b);
}
}
}
#[cfg(test)]
mod extreme_tdd_tests {
use super::*;
#[test]
fn test_color_default() {
let c = Color::default();
assert_eq!(c.r, 0);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
assert_eq!(c.a, 255);
}
#[test]
fn test_color_equality() {
let c1 = Color::rgb(100, 150, 200);
let c2 = Color::rgb(100, 150, 200);
assert_eq!(c1, c2);
}
#[test]
fn test_color_inequality() {
let c1 = Color::rgb(100, 150, 200);
let c2 = Color::rgb(100, 151, 200);
assert_ne!(c1, c2);
}
#[test]
fn test_color_clone() {
let c1 = Color::rgba(10, 20, 30, 40);
let c2 = c1;
assert_eq!(c1, c2);
}
#[test]
fn test_color_to_f32_black() {
let c = Color::BLACK;
let f = c.to_f32();
assert!((f[0] - 0.0).abs() < 1e-5);
assert!((f[1] - 0.0).abs() < 1e-5);
assert!((f[2] - 0.0).abs() < 1e-5);
assert!((f[3] - 1.0).abs() < 1e-5);
}
#[test]
fn test_color_to_f32_white() {
let c = Color::WHITE;
let f = c.to_f32();
assert!((f[0] - 1.0).abs() < 1e-5);
assert!((f[1] - 1.0).abs() < 1e-5);
assert!((f[2] - 1.0).abs() < 1e-5);
assert!((f[3] - 1.0).abs() < 1e-5);
}
#[test]
fn test_color_to_f32_transparent() {
let c = Color::TRANSPARENT;
let f = c.to_f32();
assert!((f[3] - 0.0).abs() < 1e-5);
}
#[test]
fn test_parse_hex_color_lowercase() {
let c = parse_hex_color("ff8000").unwrap();
assert_eq!(c.r, 255);
assert_eq!(c.g, 128);
assert_eq!(c.b, 0);
}
#[test]
fn test_parse_hex_color_with_hash() {
let c = parse_hex_color("#00FF00").unwrap();
assert_eq!(c.r, 0);
assert_eq!(c.g, 255);
assert_eq!(c.b, 0);
}
#[test]
fn test_parse_hex_color_invalid_length_3() {
assert!(parse_hex_color("FFF").is_err());
}
#[test]
fn test_parse_hex_color_invalid_length_5() {
assert!(parse_hex_color("FFFFF").is_err());
}
#[test]
fn test_parse_hex_color_invalid_length_9() {
assert!(parse_hex_color("FFFFFFFFF").is_err());
}
#[test]
fn test_parse_hex_color_invalid_chars() {
assert!(parse_hex_color("GGGGGG").is_err());
assert!(parse_hex_color("ZZZZZZ").is_err());
}
#[test]
fn test_color_debug() {
let c = Color::rgb(10, 20, 30);
let debug_str = format!("{:?}", c);
assert!(debug_str.contains("Color"));
assert!(debug_str.contains("10"));
assert!(debug_str.contains("20"));
assert!(debug_str.contains("30"));
}
#[test]
fn test_all_color_constants() {
assert_eq!(Color::WHITE.r, 255);
assert_eq!(Color::WHITE.g, 255);
assert_eq!(Color::WHITE.b, 255);
assert_eq!(Color::BLACK.r, 0);
assert_eq!(Color::BLACK.g, 0);
assert_eq!(Color::BLACK.b, 0);
assert_eq!(Color::RED.r, 255);
assert_eq!(Color::RED.g, 0);
assert_eq!(Color::RED.b, 0);
assert_eq!(Color::GREEN.r, 0);
assert_eq!(Color::GREEN.g, 255);
assert_eq!(Color::GREEN.b, 0);
assert_eq!(Color::BLUE.r, 0);
assert_eq!(Color::BLUE.g, 0);
assert_eq!(Color::BLUE.b, 255);
assert_eq!(Color::TRANSPARENT.a, 0);
}
}