diff --git a/examples/ui.rs b/examples/ui.rs index b2256f7..e2f92d3 100644 --- a/examples/ui.rs +++ b/examples/ui.rs @@ -15,8 +15,8 @@ pub async fn index_page(ctx: RequestContext) -> StringResponse { h1 { "Hello World!" }; ( - Screen::medium(Hover(Background(Red::_700, Nothing()))).on( - Background(Blue::_700, Text("HELLO!")) + Screen::medium(Hover(Background(Nothing()).color(Red::_700))).on( + Background(Text("HELLO!")).color(Blue::_700) ) ) diff --git a/src/lib.rs b/src/lib.rs index 1985369..809b519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(const_vec_string_slice)] + use tokio::sync::OnceCell; pub mod auth; diff --git a/src/ui/color.rs b/src/ui/color.rs index c42e898..e7baa05 100644 --- a/src/ui/color.rs +++ b/src/ui/color.rs @@ -12,7 +12,7 @@ pub trait ColorCircle { fn next(&self) -> Self; } -// todo : specific colors rgb +// todo : specific colors rgb -[#50d71e] macro_rules! color_map { ($name:ident, $id:literal) => { @@ -133,3 +133,81 @@ impl UIColor for Colors { } } } + +// TODO : Gradient + +pub struct Gradient { + start: Box, + middle: Option>, + end: Option>, + pos_start: Option, + pos_middle: Option, + pos_end: Option, +} + +impl Gradient { + pub fn from(start: C) -> Self { + Self { + start: Box::new(start), + middle: None, + end: None, + pos_end: None, + pos_middle: None, + pos_start: None, + } + } + + pub fn via(mut self, middle: C) -> Self { + self.middle = Some(Box::new(middle)); + self + } + + pub fn to(mut self, end: C) -> Self { + self.end = Some(Box::new(end)); + self + } + + pub fn step_start(mut self, percentage: u8) -> Self { + assert!(percentage <= 100, "Percentage should be under 100%"); + self.pos_start = Some(percentage); + self + } + + pub fn step_middle(mut self, percentage: u8) -> Self { + assert!(percentage <= 100, "Percentage should be under 100%"); + self.pos_middle = Some(percentage); + self + } + + pub fn step_end(mut self, percentage: u8) -> Self { + assert!(percentage <= 100, "Percentage should be under 100%"); + self.pos_end = Some(percentage); + self + } + + pub fn color_class(&self) -> Vec { + let mut classes = vec![format!("from-{}", self.start.color_class())]; + + if let Some(via) = &self.middle { + classes.push(format!("via-{}", via.color_class())); + } + + if let Some(end) = &self.end { + classes.push(format!("to-{}", end.color_class())); + } + + if let Some(step) = &self.pos_start { + classes.push(format!("from-{step}%")); + } + + if let Some(step) = &self.pos_middle { + classes.push(format!("via-{step}%")); + } + + if let Some(step) = &self.pos_end { + classes.push(format!("to-{step}%")); + } + + classes + } +} diff --git a/src/ui/components/appbar.rs b/src/ui/components/appbar.rs index 50fbbb8..8145568 100644 --- a/src/ui/components/appbar.rs +++ b/src/ui/components/appbar.rs @@ -38,9 +38,8 @@ impl UIWidget for AppBarWidget { } fn render_with_class(&self, _: &str) -> Markup { - Padding(Shadow::medium(Background( - Gray::_800, - Header( + Padding(Shadow::medium( + Background(Header( Padding( Flex( Div() @@ -70,8 +69,9 @@ impl UIWidget for AppBarWidget { .items_center(), ) .x(ScreenValue::_6), - ), - ))) + )) + .color(Gray::_800), + )) .y(ScreenValue::_2) .render() } diff --git a/src/ui/components/shell.rs b/src/ui/components/shell.rs index bdcca62..2bc0396 100644 --- a/src/ui/components/shell.rs +++ b/src/ui/components/shell.rs @@ -1,5 +1,7 @@ use maud::{PreEscaped, html}; +// TODO : refactor shell + /// Represents the HTML structure of a page shell, including the head, body class, and body content. /// /// This structure is used to construct the overall HTML structure of a page, making it easier to generate consistent HTML pages dynamically. diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 602b811..ab1717d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,5 @@ use components::Shell; -use maud::{Markup, PreEscaped, Render}; -use prelude::Text; +use maud::{Markup, PreEscaped, Render, html}; // UI @@ -17,15 +16,26 @@ pub mod components; pub mod prelude { pub use super::color::*; pub use super::primitives::Context; + pub use super::primitives::NoBrowserAppearance; pub use super::primitives::Nothing; pub use super::primitives::Side; pub use super::primitives::Size; pub use super::primitives::animation::{Animated, Animation, Delay, Duration, Scope, Timing}; pub use super::primitives::aspect::Aspect; pub use super::primitives::background::Background; + pub use super::primitives::border::{ + Border, BorderSide, BorderSize, BorderStyle, Outline, OutlineStyle, Ring, + }; pub use super::primitives::container::Container; - pub use super::primitives::cursor::Cursor; + pub use super::primitives::cursor::{Action, Cursor, TouchAction}; + pub use super::primitives::display::{ + BoxDecorationBreak, BoxSizing, BreakAfter, BreakBefore, BreakInside, BreakInsideValue, + BreakValue, Clear, Display, Float, ObjectFit, Overflow, + }; pub use super::primitives::div::Div; + pub use super::primitives::filter::{ + Blur, Brightness, Contrast, Grayscale, HueRotate, Invert, Saturate, Sepia, + }; pub use super::primitives::flex::{ Direction, Flex, FlexBasis, FlexGrow, Justify, Order, Strategy, Wrap, }; @@ -35,16 +45,20 @@ pub mod prelude { pub use super::primitives::link::Link; pub use super::primitives::margin::Margin; pub use super::primitives::padding::Padding; + pub use super::primitives::position::{Position, PositionKind, Resize, Resizeable}; pub use super::primitives::rounded::Rounded; pub use super::primitives::script; + pub use super::primitives::scroll::{Overscroll, Scroll, SnapAlign, SnapType}; pub use super::primitives::shadow::Shadow; pub use super::primitives::sized::Sized; pub use super::primitives::space::{ScreenValue, SpaceBetween}; + pub use super::primitives::svg::SVG; pub use super::primitives::text::{ - Code, DecorationKind, DecorationStyle, DecorationThickness, LetterSpacing, LineClamp, - LineHeight, ListStyle, NumberStyle, Paragraph, Span, Text, TextAlignment, TextContent, - TextDecoration, TextHyphens, TextOverflow, TextTransform, TextWhitespace, TextWordBreak, - TextWrap, UnderlineOffset, VerticalTextAlignment, + AccentColor, Code, DecorationKind, DecorationStyle, DecorationThickness, LetterSpacing, + LineClamp, LineHeight, ListStyle, NumberStyle, Paragraph, Span, Text, TextAlignment, + TextContent, TextCursorColor, TextDecoration, TextHyphens, TextOverflow, TextSelection, + TextTransform, TextWhitespace, TextWordBreak, TextWrap, UnderlineOffset, + VerticalTextAlignment, }; pub use super::primitives::transform::{ RenderTransformCPU, RenderTransformGPU, Rotate, Scale, Skew, Transform, TransformOrigin, @@ -139,37 +153,37 @@ impl UIWidget for PreEscaped { impl UIWidget for String { fn can_inherit(&self) -> bool { - Text(&self).can_inherit() + false } fn base_class(&self) -> Vec { - Text(&self).base_class() + Vec::new() } fn extended_class(&self) -> Vec { - Text(&self).extended_class() + Vec::new() } - fn render_with_class(&self, class: &str) -> Markup { - Text(&self).render_with_class(class) + fn render_with_class(&self, _: &str) -> Markup { + html!((self)) } } impl UIWidget for &str { fn can_inherit(&self) -> bool { - Text(&self).can_inherit() + false } fn base_class(&self) -> Vec { - Text(&self).base_class() + Vec::new() } fn extended_class(&self) -> Vec { - Text(&self).extended_class() + Vec::new() } - fn render_with_class(&self, class: &str) -> Markup { - Text(&self).render_with_class(class) + fn render_with_class(&self, _: &str) -> Markup { + html!((self)) } } diff --git a/src/ui/primitives/background.rs b/src/ui/primitives/background.rs index e07503e..dbd000f 100644 --- a/src/ui/primitives/background.rs +++ b/src/ui/primitives/background.rs @@ -1,16 +1,98 @@ use maud::{Markup, Render, html}; -use crate::ui::{UIWidget, color::UIColor}; +use crate::ui::{ + UIWidget, + color::{Gradient, UIColor}, +}; #[allow(non_snake_case)] -pub fn Background( - color: C, - inner: T, -) -> BackgroundWidget { - BackgroundWidget(Box::new(inner), Box::new(color)) +pub fn Background(inner: T) -> BackgroundWidget { + BackgroundWidget( + Box::new(inner), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) } -pub struct BackgroundWidget(Box, Box); +pub struct BackgroundWidget( + // Inner + Box, + // Background Color + Option>, + // Background Attachment + Option, + Option, + Option, + Option, + Option, + // Background Image URL + Option, + // Gradient + Option, + Option, + Option, +); + +impl BackgroundWidget { + pub fn color(mut self, color: C) -> Self { + self.1 = Some(Box::new(color)); + self + } + + pub fn image(mut self, url: &str) -> Self { + self.7 = Some(url.to_string()); + self + } + + pub fn none(mut self) -> Self { + self.8 = Some(BackgroundGradient::None); + self + } + + pub fn gradient(mut self, direction: BackgroundGradient, gradient: Gradient) -> Self { + self.8 = Some(direction); + self.9 = Some(gradient); + self + } + + pub fn position(mut self, position: BackgroundPosition) -> Self { + self.10 = Some(position); + self + } + + pub fn scroll(mut self, attachment: BackgroundScrollAttachment) -> Self { + self.2 = Some(attachment); + self + } + + pub fn clip(mut self, clip: BackgroundClip) -> Self { + self.3 = Some(clip); + self + } + + pub fn origin(mut self, origin: BackgroundOrigin) -> Self { + self.4 = Some(origin); + self + } + + pub fn repeat(mut self, repeat: BackgroundRepeat) -> Self { + self.5 = Some(repeat); + self + } + + pub fn size(mut self, size: BackgroundSize) -> Self { + self.6 = Some(size); + self + } +} impl Render for BackgroundWidget { fn render(&self) -> Markup { @@ -24,7 +106,49 @@ impl UIWidget for BackgroundWidget { } fn base_class(&self) -> Vec { - vec![format!("bg-{}", self.1.color_class())] + let mut ret = Vec::new(); + + if let Some(color) = &self.1 { + ret.push(format!("bg-{}", color.color_class())); + } + + if let Some(attachment) = &self.2 { + ret.push(attachment.to_value().to_string()); + } + + if let Some(clip) = &self.3 { + ret.push(clip.to_value().to_string()); + } + + if let Some(origin) = &self.4 { + ret.push(origin.to_value().to_string()); + } + + if let Some(repeat) = &self.5 { + ret.push(repeat.to_value().to_string()); + } + + if let Some(size) = &self.6 { + ret.push(size.to_value().to_string()); + } + + if let Some(image) = &self.7 { + ret.push(format!("bg-[url('{image}')]")); + } + + if let Some(gradient) = &self.8 { + ret.push(gradient.to_value().to_string()); + } + + if let Some(gradient) = &self.9 { + ret.extend_from_slice(&gradient.color_class()); + } + + if let Some(position) = &self.10 { + ret.push(position.to_value().to_string()); + } + + ret } fn extended_class(&self) -> Vec { @@ -47,3 +171,151 @@ impl UIWidget for BackgroundWidget { } } } + +/// Controlling how a background image behaves when scrolling. +pub enum BackgroundScrollAttachment { + /// Fix the background image relative to the viewport. + Fixed, + /// Scroll the background image with the container and the viewport. + Local, + /// Scroll the background image with the viewport, but not with the container. + Scroll, +} + +impl BackgroundScrollAttachment { + pub const fn to_value(&self) -> &str { + match self { + BackgroundScrollAttachment::Fixed => "bg-fixed", + BackgroundScrollAttachment::Local => "bg-local", + BackgroundScrollAttachment::Scroll => "bg-scroll", + } + } +} + +pub enum BackgroundClip { + Border, + Padding, + Content, + Text, +} + +impl BackgroundClip { + pub const fn to_value(&self) -> &str { + match self { + BackgroundClip::Border => "bg-clip-border", + BackgroundClip::Padding => "bg-clip-padding", + BackgroundClip::Content => "bg-clip-content", + BackgroundClip::Text => "bg-clip-text", + } + } +} + +pub enum BackgroundOrigin { + Border, + Padding, + Content, +} + +impl BackgroundOrigin { + pub const fn to_value(&self) -> &str { + match self { + BackgroundOrigin::Border => "bg-origin-border", + BackgroundOrigin::Padding => "bg-origin-padding", + BackgroundOrigin::Content => "bg-origin-content", + } + } +} + +pub enum BackgroundRepeat { + Repeat, + NoRepeat, + RepeatX, + RepeatY, + Round, + Space, +} + +impl BackgroundRepeat { + pub const fn to_value(&self) -> &str { + match self { + BackgroundRepeat::Repeat => "bg-repeat", + BackgroundRepeat::NoRepeat => "bg-no-repeat", + BackgroundRepeat::RepeatX => "bg-repeat-x", + BackgroundRepeat::RepeatY => "bg-repeat-y", + BackgroundRepeat::Round => "bg-repeat-round", + BackgroundRepeat::Space => "bg-repeat-space", + } + } +} + +pub enum BackgroundSize { + Auto, + Cover, + Contain, +} + +impl BackgroundSize { + pub const fn to_value(&self) -> &str { + match self { + BackgroundSize::Auto => "bg-auto", + BackgroundSize::Cover => "bg-cover", + BackgroundSize::Contain => "bg-contain", + } + } +} + +pub enum BackgroundGradient { + None, + ToTop, + ToTopRight, + ToRight, + ToBottomRight, + ToBottom, + ToBottomLeft, + ToLeft, + ToTopLeft, +} + +impl BackgroundGradient { + pub const fn to_value(&self) -> &str { + match self { + BackgroundGradient::None => "bg-none", + BackgroundGradient::ToTop => "bg-gradient-to-t", + BackgroundGradient::ToTopRight => "bg-gradient-to-tr", + BackgroundGradient::ToRight => "bg-gradient-to-r", + BackgroundGradient::ToBottomRight => "bg-gradient-to-br", + BackgroundGradient::ToBottom => "bg-gradient-to-b", + BackgroundGradient::ToBottomLeft => "bg-gradient-to-bl", + BackgroundGradient::ToLeft => "bg-gradient-to-l", + BackgroundGradient::ToTopLeft => "bg-gradient-to-tl", + } + } +} + +pub enum BackgroundPosition { + Bottom, + Center, + Left, + LeftBottom, + LeftTop, + Right, + RightBottom, + RightTop, + Top, +} + +impl BackgroundPosition { + pub const fn to_value(&self) -> &str { + match self { + BackgroundPosition::Bottom => "bg-bottom", + BackgroundPosition::Center => "bg-center", + BackgroundPosition::Left => "bg-left", + BackgroundPosition::LeftBottom => "bg-left-bottom", + BackgroundPosition::LeftTop => "bg-left-top", + BackgroundPosition::Right => "bg-right", + BackgroundPosition::RightBottom => "bg-right-bottom", + BackgroundPosition::RightTop => "bg-right-top", + BackgroundPosition::Top => "bg-top", + } + } +} diff --git a/src/ui/primitives/border.rs b/src/ui/primitives/border.rs new file mode 100644 index 0000000..c7e14c3 --- /dev/null +++ b/src/ui/primitives/border.rs @@ -0,0 +1,397 @@ +use maud::{Markup, Render, html}; + +use crate::ui::{UIWidget, color::UIColor}; + +pub enum BorderSize { + _0, + _2, + _4, + _8, +} + +impl BorderSize { + pub const fn to_value(&self) -> &str { + match self { + BorderSize::_0 => "0", + BorderSize::_2 => "2", + BorderSize::_4 => "4", + BorderSize::_8 => "8", + } + } +} + +pub enum BorderSide { + X, + Y, + Start, + End, + Top, + Right, + Bottom, + Left, +} + +impl BorderSide { + pub const fn to_value(&self) -> &str { + match self { + BorderSide::X => "x", + BorderSide::Y => "y", + BorderSide::Start => "s", + BorderSide::End => "e", + BorderSide::Top => "t", + BorderSide::Right => "r", + BorderSide::Bottom => "b", + BorderSide::Left => "l", + } + } +} + +pub enum BorderStyle { + Solid, + Dashed, + Dotted, + Double, + Hidden, + None, +} + +impl BorderStyle { + pub const fn to_value(&self) -> &str { + match self { + BorderStyle::Solid => "border-solid", + BorderStyle::Dashed => "border-dashed", + BorderStyle::Dotted => "border-dotted", + BorderStyle::Double => "border-double", + BorderStyle::Hidden => "border-hidden", + BorderStyle::None => "border-none", + } + } +} + +#[allow(non_snake_case)] +pub fn Border(inner: T) -> BorderWidget { + BorderWidget(Box::new(inner), None, None, None, None) +} + +pub struct BorderWidget( + Box, + Option, + Option, + Option>, + Option, +); + +impl BorderWidget { + #[must_use] + pub fn size(mut self, size: BorderSize) -> Self { + self.1 = Some(size); + self + } + + #[must_use] + pub const fn side(mut self, side: BorderSide) -> Self { + self.2 = Some(side); + self + } + + #[must_use] + pub const fn style(mut self, style: BorderStyle) -> Self { + self.4 = Some(style); + self + } + + #[must_use] + pub fn color(mut self, color: C) -> Self { + self.3 = Some(Box::new(color)); + self + } + + fn border_class(&self) -> String { + if let Some(side) = &self.2 { + if let Some(size) = &self.1 { + return format!("border-{}-{}", side.to_value(), size.to_value()); + } + } else if let Some(size) = &self.1 { + return format!("border-{}", size.to_value()); + } + + "border".to_owned() + } +} + +impl Render for BorderWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for BorderWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let mut ret = vec![self.border_class()]; + + if let Some(color) = &self.3 { + ret.push(format!("border-{}", color.color_class())); + } + + if let Some(style) = &self.4 { + ret.push(style.to_value().to_string()); + } + + ret + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum OutlineStyle { + Solid, + Dashed, + Dotted, + Double, + None, +} + +impl OutlineStyle { + pub const fn to_value(&self) -> &str { + match self { + OutlineStyle::Solid => "outline", + OutlineStyle::Dashed => "outline-dashed", + OutlineStyle::Dotted => "outline-dotted", + OutlineStyle::Double => "outline-double", + OutlineStyle::None => "outline-none", + } + } +} + +#[allow(non_snake_case)] +pub fn Outline(width: u32, inner: T) -> OutlineWidget { + OutlineWidget(Box::new(inner), width, None, None, 0) +} + +pub struct OutlineWidget( + Box, + u32, + Option>, + Option, + u32, +); + +impl OutlineWidget { + #[must_use] + pub const fn offset(mut self, offset: u32) -> Self { + self.4 = offset; + self + } + + #[must_use] + pub const fn style(mut self, style: OutlineStyle) -> Self { + self.3 = Some(style); + self + } + + #[must_use] + pub fn color(mut self, color: C) -> Self { + self.2 = Some(Box::new(color)); + self + } +} + +impl Render for OutlineWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for OutlineWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let class = match self.1 { + 0 => "outline-0", + 1 => "outline-1", + 2 => "outline-2", + 4 => "outline-4", + 8 => "outline-8", + _ => &format!("outline-[{}px]", self.1), + }; + + let mut ret = vec![class.to_string()]; + + if let Some(color) = &self.2 { + ret.push(format!("outline-{}", color.color_class())); + } + + if let Some(style) = &self.3 { + ret.push(style.to_value().to_string()); + } + + ret.push(match self.4 { + 0 => "outline-offset-0".to_string(), + 1 => "outline-offset-1".to_string(), + 2 => "outline-offset-2".to_string(), + 4 => "outline-offset-4".to_string(), + 8 => "outline-offset-8".to_string(), + _ => format!("outline-offset-[{}px]", self.4), + }); + + ret + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +#[allow(non_snake_case)] +pub fn Ring(width: u32, inner: T) -> RingWidget { + RingWidget(Box::new(inner), width, None, false, 0, None) +} + +pub struct RingWidget( + // Inner + Box, + // Size + u32, + // Color + Option>, + // Inset + bool, + // Offset Width + u32, + // Offset Color + Option>, +); + +impl RingWidget { + #[must_use] + pub const fn inset(mut self) -> Self { + self.3 = true; + self + } + + #[must_use] + pub const fn offset_width(mut self, offset: u32) -> Self { + self.4 = offset; + self + } + + #[must_use] + pub fn offset_color(mut self, color: C) -> Self { + self.5 = Some(Box::new(color)); + self + } + + #[must_use] + pub fn color(mut self, color: C) -> Self { + self.2 = Some(Box::new(color)); + self + } +} + +impl Render for RingWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for RingWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let class = match self.1 { + 0 => "ring-0", + 1 => "ring-1", + 2 => "ring-2", + 4 => "ring-4", + 8 => "ring-8", + _ => &format!("ring-[{}px]", self.1), + }; + + let mut ret = vec![class.to_string()]; + + if let Some(color) = &self.2 { + ret.push(format!("ring-{}", color.color_class())); + } + + if self.3 { + ret.push("ring-inset".to_string()); + } + + ret.push(match self.4 { + 0 => "ring-offset-0".to_string(), + 1 => "ring-offset-1".to_string(), + 2 => "ring-offset-2".to_string(), + 4 => "ring-offset-4".to_string(), + 8 => "ring-offset-8".to_string(), + _ => format!("ring-offset-[{}px]", self.4), + }); + + if let Some(color) = &self.5 { + ret.push(format!("ring-offset-{}", color.color_class())); + } + + ret + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/primitives/cursor.rs b/src/ui/primitives/cursor.rs index 1ab521e..d6d5141 100644 --- a/src/ui/primitives/cursor.rs +++ b/src/ui/primitives/cursor.rs @@ -122,3 +122,76 @@ impl UIWidget for CursorWidget { } } } + +#[allow(non_snake_case)] +pub fn TouchAction(action: Action, inner: T) -> TouchActionWidget { + TouchActionWidget(Box::new(inner), action) +} + +pub struct TouchActionWidget(Box, Action); + +impl Render for TouchActionWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for TouchActionWidget { + fn can_inherit(&self) -> bool { + false + } + + fn base_class(&self) -> Vec { + vec![self.1.to_value().to_string()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum Action { + Auto, + None, + PanX, + PanLeft, + PanRight, + PanY, + PanUp, + PanDown, + PinchZoom, + Manipulation, +} + +impl Action { + pub const fn to_value(&self) -> &str { + match self { + Action::Auto => "touch-auto", + Action::None => "touch-none", + Action::PanX => "touch-pan-x", + Action::PanLeft => "touch-pan-left", + Action::PanRight => "touch-pan-right", + Action::PanY => "touch-pan-y", + Action::PanUp => "touch-pan-up", + Action::PanDown => "touch-pan-down", + Action::PinchZoom => "touch-pinch-zoom", + Action::Manipulation => "touch-manipulation", + } + } +} diff --git a/src/ui/primitives/display.rs b/src/ui/primitives/display.rs new file mode 100644 index 0000000..287bd59 --- /dev/null +++ b/src/ui/primitives/display.rs @@ -0,0 +1,314 @@ +use crate::ui::UIWidget; +use maud::{Markup, Render, html}; + +macro_rules! string_class_widget { + ($name:ident) => { + pub struct $name(Box, String); + + impl Render for $name { + fn render(&self) -> Markup { + self.render_with_class("") + } + } + + impl UIWidget for $name { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![self.1.clone()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } + } + }; +} + +macro_rules! constructor { + ($name:ident, $class:literal) => { + #[allow(non_snake_case)] + pub fn $name(inner: T) -> Self { + Self(Box::new(inner), $class.to_string()) + } + }; +} + +pub enum BreakValue { + Auto, + Avoid, + All, + AvoidPage, + Page, + Left, + Right, + Column, +} + +impl BreakValue { + pub const fn to_value(&self) -> &str { + match self { + BreakValue::Auto => "auto", + BreakValue::Avoid => "avoid", + BreakValue::All => "all", + BreakValue::AvoidPage => "break-page", + BreakValue::Page => "page", + BreakValue::Left => "left", + BreakValue::Right => "right", + BreakValue::Column => "column", + } + } +} + +#[allow(non_snake_case)] +pub fn BreakAfter(value: BreakValue, inner: T) -> BreakWidget { + BreakWidget(Box::new(inner), false, value) +} + +#[allow(non_snake_case)] +pub fn BreakBefore(value: BreakValue, inner: T) -> BreakWidget { + BreakWidget(Box::new(inner), true, value) +} + +pub struct BreakWidget(Box, bool, BreakValue); + +impl Render for BreakWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for BreakWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + if self.1 { + vec![format!("break-before-{}", self.2.to_value())] + } else { + vec![format!("break-after-{}", self.2.to_value())] + } + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum BreakInsideValue { + Auto, + Avoid, + AvoidPage, + AvoidColumn, +} + +impl BreakInsideValue { + pub const fn to_value(&self) -> &str { + match self { + BreakInsideValue::Auto => "break-inside-auto", + BreakInsideValue::Avoid => "break-inside-avoid", + BreakInsideValue::AvoidPage => "break-inside-avoid-page", + BreakInsideValue::AvoidColumn => "break-inside-avoid-column", + } + } +} + +#[allow(non_snake_case)] +pub fn BreakInside(value: BreakValue, inner: T) -> BreakWidget { + BreakWidget(Box::new(inner), true, value) +} + +pub struct BreakInsideWidget(Box, BreakValue); + +impl Render for BreakInsideWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for BreakInsideWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![self.1.to_value().to_string()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +string_class_widget!(BoxDecorationBreak); + +impl BoxDecorationBreak { + constructor!(Clone, "box-decoration-clone"); + constructor!(Slice, "box-decoration-slice"); +} + +string_class_widget!(BoxSizing); + +impl BoxSizing { + constructor!(Border, "box-border"); + constructor!(Content, "box-content"); +} + +string_class_widget!(Display); + +impl Display { + constructor!(Block, "block"); + constructor!(InlineBlock, "inline-block"); + constructor!(Inline, "inline"); + constructor!(Flex, "flex"); + constructor!(InlineFlex, "inline-flex"); + constructor!(Table, "table"); + constructor!(InlineTable, "inline-table"); + constructor!(TableCaption, "table-caption"); + constructor!(TableCell, "table-cell"); + constructor!(TableColumn, "table-column"); + constructor!(TableColumnGroup, "table-column-group"); + constructor!(TableFooterGroup, "table-footer-group"); + constructor!(TableHeaderGroup, "table-header-group"); + constructor!(TableRowGroup, "table-row-group"); + constructor!(TableRow, "table-row"); + constructor!(FlowRoot, "flow-root"); + constructor!(Grid, "grid"); + constructor!(InlineGrid, "inline-grid"); + constructor!(Contents, "contents"); + constructor!(ListItem, "list-item"); + constructor!(Hidden, "hidden"); +} + +string_class_widget!(Float); + +impl Float { + constructor!(Start, "float-start"); + constructor!(End, "float-end"); + constructor!(Left, "float-left"); + constructor!(Right, "float-right"); + constructor!(None, "float-none"); +} + +string_class_widget!(Clear); + +impl Clear { + constructor!(Start, "clear-start"); + constructor!(End, "clear-end"); + constructor!(Left, "clear-left"); + constructor!(Right, "clear-right"); + constructor!(Both, "clear-both"); + constructor!(None, "clear-none"); +} + +string_class_widget!(ObjectFit); + +impl ObjectFit { + constructor!(Contain, "object-contain"); + constructor!(Cover, "object-cover"); + constructor!(Fill, "object-fill"); + constructor!(None, "object-none"); + constructor!(ScaleDown, "object-scale-down"); +} + +string_class_widget!(Overflow); + +impl Overflow { + constructor!(Auto, "overflow-auto"); + constructor!(Hidden, "overflow-hidden"); + constructor!(Clip, "overflow-clip"); + constructor!(Visible, "overflow-visible"); + constructor!(Scroll, "overflow-scroll"); + constructor!(XAuto, "overflow-x-auto"); + constructor!(YAuto, "overflow-y-auto"); + constructor!(XHidden, "overflow-x-hidden"); + constructor!(YHidden, "overflow-y-hidden"); + constructor!(XClip, "overflow-x-clip"); + constructor!(YClip, "overflow-y-clip"); + constructor!(XVisible, "overflow-x-visible"); + constructor!(YVisible, "overflow-y-visible"); + constructor!(XScroll, "overflow-x-scroll"); + constructor!(YScroll, "overflow-y-scroll"); +} + +string_class_widget!(JustifySelf); + +impl JustifySelf { + constructor!(Auto, "justify-self-auto"); + constructor!(Start, "justify-self-start"); + constructor!(End, "justify-self-end"); + constructor!(Center, "justify-self-center"); + constructor!(Stretch, "justify-self-stretch"); +} + +string_class_widget!(PlaceSelf); + +impl PlaceSelf { + constructor!(Auto, "place-self-auto"); + constructor!(Start, "place-self-start"); + constructor!(End, "place-self-end"); + constructor!(Center, "place-self-center"); + constructor!(Stretch, "place-self-stretch"); +} + +string_class_widget!(AlignSelf); + +impl AlignSelf { + constructor!(Auto, "self-auto"); + constructor!(Start, "self-start"); + constructor!(End, "self-end"); + constructor!(Center, "self-center"); + constructor!(Stretch, "self-stretch"); + constructor!(Baseline, "self-baseline"); +} diff --git a/src/ui/primitives/filter.rs b/src/ui/primitives/filter.rs new file mode 100644 index 0000000..3218387 --- /dev/null +++ b/src/ui/primitives/filter.rs @@ -0,0 +1,479 @@ +use crate::ui::UIWidget; +use maud::{Markup, Render, html}; + +use super::Size; + +#[allow(non_snake_case)] +pub fn Blur(amount: Size, inner: T) -> BlurWidget { + BlurWidget(Box::new(inner), amount, false) +} + +pub struct BlurWidget(Box, Size, bool); + +impl BlurWidget { + pub fn backdrop(mut self) -> Self { + self.2 = true; + self + } +} + +impl Render for BlurWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for BlurWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let class = match &self.1 { + Size::Custom(s) => &format!(" blur-[{s}]"), + Size::None => "blur-none", + Size::Small => "blur-sm", + Size::Regular => "blur", + Size::Medium => "blur-md", + Size::Large => "blur-lg", + Size::XL => "blur-xl", + Size::_2XL => "blur-2xl", + Size::_3XL => "blur-3xl", + Size::Full => "blur-3xl", + }; + + if self.2 { + return vec![format!("backdrop-{class}")]; + } + + vec![class.to_string()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +macro_rules! build_value_widget { + ($constr:ident, $widget:ident, $class:literal) => { + #[allow(non_snake_case)] + pub fn $constr(value: f64, inner: T) -> $widget { + $widget(Box::new(inner), value, false) + } + + pub struct $widget(Box, f64, bool); + + impl $widget { + pub fn backdrop(mut self) -> Self { + self.2 = true; + self + } + } + + impl Render for $widget { + fn render(&self) -> Markup { + self.render_with_class("") + } + } + + impl UIWidget for $widget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let mut ret = $class.to_string(); + ret.push_str(&format!("-[{:.2}]", self.1)); + + if self.2 { + return vec![format!("backdrop-{ret}")]; + } + + vec![ret] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } + } + }; +} + +build_value_widget!(Brightness, BrightnessWidget, "brightness"); +build_value_widget!(Contrast, ConstrastWidget, "contrast"); +build_value_widget!(Saturate, SaturationWidget, "saturate"); + +macro_rules! build_on_off_widget { + ($constr:ident, $widget:ident, $class:literal) => { + #[allow(non_snake_case)] + pub fn $constr(inner: T) -> $widget { + $widget(Box::new(inner), true, false) + } + + pub struct $widget(Box, bool, bool); + + impl $widget { + pub fn none(mut self) -> Self { + self.1 = false; + self + } + + pub fn backdrop(mut self) -> Self { + self.2 = true; + self + } + } + + impl Render for $widget { + fn render(&self) -> Markup { + self.render_with_class("") + } + } + + impl UIWidget for $widget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let class = if self.1 { + $class.to_string() + } else { + concat!($class, "-0").to_string() + }; + + if self.2 { + return vec![format!("backdrop-{class}")]; + } + + vec![class] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } + } + }; +} + +build_on_off_widget!(Grayscale, GrayscaleWidget, "grayscale"); +build_on_off_widget!(Invert, InvertWidget, "invert"); +build_on_off_widget!(Sepia, SepiaWidget, "sepia"); + +#[allow(non_snake_case)] +pub fn HueRotate(deg: u32, inner: T) -> HueRotateWidget { + HueRotateWidget(Box::new(inner), deg, false) +} + +pub struct HueRotateWidget(Box, u32, bool); + +impl HueRotateWidget { + pub fn backdrop(mut self) -> Self { + self.2 = true; + self + } +} + +impl Render for HueRotateWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for HueRotateWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let class = format!("hue-rotate-[{:.2}deg]", self.1); + + if self.2 { + return vec![format!("backdrop-{class}")]; + } + + vec![class] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +#[allow(non_snake_case)] +pub fn Opacity(value: f64, inner: T) -> OpacityWidget { + OpacityWidget(Box::new(inner), value, false) +} + +pub struct OpacityWidget(Box, f64, bool); + +impl OpacityWidget { + pub fn backdrop(mut self) -> Self { + self.2 = true; + self + } +} + +impl Render for OpacityWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for OpacityWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let class = match self.1 { + 0.0 => "opacity-0", + 0.05 => "opacity-5", + 0.1 => "opacity-10", + 0.15 => "opacity-15", + 0.2 => "opacity-20", + 0.25 => "opacity-25", + 0.3 => "opacity-30", + 0.35 => "opacity-35", + 0.4 => "opacity-40", + 0.45 => "opacity-45", + 0.5 => "opacity-50", + 0.55 => "opacity-55", + 0.6 => "opacity-60", + 0.65 => "opacity-65", + 0.7 => "opacity-70", + 0.75 => "opacity-75", + 0.8 => "opacity-80", + 0.85 => "opacity-85", + 0.9 => "opacity-90", + 0.95 => "opacity-95", + 1.0 => "opacity-100", + _ => &format!("opacity-[{:.2}]", self.1), + }; + + if self.2 { + return vec![format!("backdrop-{class}")]; + } + + vec![class.to_string()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, + PlusDarker, + PlusLighter, +} + +impl BlendMode { + pub const fn to_value(&self) -> &str { + match self { + BlendMode::Normal => "normal", + BlendMode::Multiply => "multiply", + BlendMode::Screen => "screen", + BlendMode::Overlay => "overlay", + BlendMode::Darken => "darken", + BlendMode::Lighten => "lighten", + BlendMode::ColorDodge => "color-dodge", + BlendMode::ColorBurn => "color-burn", + BlendMode::HardLight => "hard-light", + BlendMode::SoftLight => "soft-light", + BlendMode::Difference => "difference", + BlendMode::Exclusion => "exclusion", + BlendMode::Hue => "hue", + BlendMode::Saturation => "saturation", + BlendMode::Color => "color", + BlendMode::Luminosity => "luminosity", + BlendMode::PlusDarker => "plus-darker", + BlendMode::PlusLighter => "plus-lighter", + } + } +} + +#[allow(non_snake_case)] +pub fn MixBlendMode(mode: BlendMode, inner: T) -> MixBlendModeWidget { + MixBlendModeWidget(Box::new(inner), mode) +} + +pub struct MixBlendModeWidget(Box, BlendMode); + +impl Render for MixBlendModeWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for MixBlendModeWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![format!("mix-blend-{}", self.1.to_value())] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +#[allow(non_snake_case)] +pub fn BackgroundBlendMode( + mode: BlendMode, + inner: T, +) -> BackgroundBlendModeWidget { + BackgroundBlendModeWidget(Box::new(inner), mode) +} + +pub struct BackgroundBlendModeWidget(Box, BlendMode); + +impl Render for BackgroundBlendModeWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for BackgroundBlendModeWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![format!("bg-blend-{}", self.1.to_value())] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/primitives/flex.rs b/src/ui/primitives/flex.rs index b662362..35b231e 100644 --- a/src/ui/primitives/flex.rs +++ b/src/ui/primitives/flex.rs @@ -1,11 +1,11 @@ -use crate::ui::UIWidget; +use crate::ui::{UIWidget, color::UIColor}; use maud::{Markup, Render, html}; use super::space::{Fraction, ScreenValue}; #[allow(non_snake_case)] pub fn Flex(inner: T) -> FlexWidget { - FlexWidget(Box::new(inner), vec![], false) + FlexWidget(Box::new(inner), vec![], false, None) } pub enum Justify { @@ -19,7 +19,7 @@ pub enum Justify { Stretch, } -pub struct FlexWidget(Box, Vec, bool); +pub struct FlexWidget(Box, Vec, bool, Option); impl Render for FlexWidget { fn render(&self) -> Markup { @@ -41,9 +41,65 @@ impl FlexWidget { self } + #[must_use] + pub fn divide_style(mut self, style: DivideStyle) -> Self { + self.1.push(style.to_value().to_string()); + self + } + + #[must_use] + pub fn divide_color(mut self, color: C) -> Self { + self.1.push(format!("divide-{}", color.color_class())); + self + } + + #[must_use] + pub fn divide_x(mut self, width: DivideWidth) -> Self { + let reversed = self + .3 + .as_ref() + .map(|x| match x { + Direction::Row => false, + Direction::RowReverse => true, + Direction::Column => false, + Direction::ColumnReverse => true, + }) + .unwrap_or_default(); + + self.1.push(format!("divide-x-{}", width.to_value())); + + if reversed { + self.1.push("divide-x-reverse".to_string()); + } + + self + } + + #[must_use] + pub fn divide_y(mut self, width: DivideWidth) -> Self { + let reversed = self + .3 + .as_ref() + .map(|x| match x { + Direction::Row => false, + Direction::RowReverse => true, + Direction::Column => false, + Direction::ColumnReverse => true, + }) + .unwrap_or_default(); + + self.1.push(format!("divide-y-{}", width.to_value())); + + if reversed { + self.1.push("divide-y-reverse".to_string()); + } + + self + } + #[must_use] pub fn direction(mut self, direction: Direction) -> Self { - self.1.push(format!("flex-{}", direction.to_value())); + self.3 = Some(direction); self } @@ -53,6 +109,24 @@ impl FlexWidget { self } + #[must_use] + pub fn justify_items(mut self, justify: JustifyItems) -> Self { + self.1.push(justify.to_value().to_string()); + self + } + + #[must_use] + pub fn align_content(mut self, align: AlignContent) -> Self { + self.1.push(align.to_value().to_string()); + self + } + + #[must_use] + pub fn align_items(mut self, align: AlignItems) -> Self { + self.1.push(align.to_value().to_string()); + self + } + #[must_use] pub fn justify(mut self, value: Justify) -> Self { let class = match value { @@ -95,6 +169,26 @@ impl FlexWidget { } } +pub enum DivideWidth { + Custom(u64), + _0, + _2, + _4, + _8, +} + +impl DivideWidth { + pub fn to_value(&self) -> String { + match self { + DivideWidth::Custom(s) => format!("[{s}px]"), + DivideWidth::_0 => "0".to_string(), + DivideWidth::_2 => "2".to_string(), + DivideWidth::_4 => "4".to_string(), + DivideWidth::_8 => "8".to_string(), + } + } +} + pub enum Direction { Row, RowReverse, @@ -136,6 +230,11 @@ impl UIWidget for FlexWidget { fn base_class(&self) -> Vec { let mut res = vec!["flex".to_string()]; + + if let Some(direction) = &self.3 { + res.push(format!("flex-{}", direction.to_value())); + } + res.extend_from_slice(&self.1); res } @@ -399,3 +498,89 @@ impl UIWidget for OrderWidget { } } } + +pub enum DivideStyle { + Solid, + Dashed, + Dotted, + Double, + None, +} + +impl DivideStyle { + pub const fn to_value(&self) -> &str { + match self { + DivideStyle::Solid => "divide-solid", + DivideStyle::Dashed => "divide-dashed", + DivideStyle::Dotted => "divide-dotted", + DivideStyle::Double => "divide-double", + DivideStyle::None => "divide-none", + } + } +} + +pub enum JustifyItems { + Start, + End, + Center, + Stretch, +} + +impl JustifyItems { + pub const fn to_value(&self) -> &str { + match self { + JustifyItems::Start => "justify-items-start", + JustifyItems::End => "justify-items-end", + JustifyItems::Center => "justify-items-center", + JustifyItems::Stretch => "justify-items-stretch", + } + } +} + +pub enum AlignContent { + Normal, + Center, + Start, + End, + Between, + Around, + Evenly, + Baseline, + Stretch, +} + +impl AlignContent { + pub const fn to_value(&self) -> &str { + match self { + AlignContent::Normal => "content-normal", + AlignContent::Center => "content-center", + AlignContent::Start => "content-start", + AlignContent::End => "content-end", + AlignContent::Between => "content-between", + AlignContent::Around => "content-around", + AlignContent::Evenly => "content-evenly", + AlignContent::Baseline => "content-baseline", + AlignContent::Stretch => "content-stretch", + } + } +} + +pub enum AlignItems { + Start, + End, + Center, + Baseline, + Stretch, +} + +impl AlignItems { + pub const fn to_value(&self) -> &str { + match self { + AlignItems::Start => "items-start", + AlignItems::End => "items-end", + AlignItems::Center => "items-center", + AlignItems::Baseline => "items-baseline", + AlignItems::Stretch => "items-stretch", + } + } +} diff --git a/src/ui/primitives/grid.rs b/src/ui/primitives/grid.rs new file mode 100644 index 0000000..cd1e345 --- /dev/null +++ b/src/ui/primitives/grid.rs @@ -0,0 +1,461 @@ +use crate::ui::{UIWidget, color::UIColor}; +use maud::{Markup, Render, html}; + +use super::{ + flex::{AlignContent, AlignItems, DivideStyle, DivideWidth, Justify, JustifyItems}, + space::ScreenValue, +}; + +#[allow(non_snake_case)] +pub fn Grid(inner: T) -> GridWidget { + GridWidget(Box::new(inner), vec![], false) +} + +pub struct GridWidget(Box, Vec, bool); + +impl Render for GridWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +pub enum GridAmount { + _1, + _2, + _3, + _4, + _5, + _6, + _7, + _8, + _9, + _10, + _11, + _12, + None, + Subgrid, +} + +impl GridAmount { + pub const fn to_value(&self) -> &str { + match self { + GridAmount::_1 => "1", + GridAmount::_2 => "2", + GridAmount::_3 => "3", + GridAmount::_4 => "4", + GridAmount::_5 => "5", + GridAmount::_6 => "6", + GridAmount::_7 => "7", + GridAmount::_8 => "8", + GridAmount::_9 => "9", + GridAmount::_10 => "10", + GridAmount::_11 => "11", + GridAmount::_12 => "12", + GridAmount::None => "none", + GridAmount::Subgrid => "subgrid", + } + } +} + +impl GridWidget { + #[must_use] + pub fn columns(mut self, amount: GridAmount) -> Self { + self.1.push(format!("grid-cols-{}", amount.to_value())); + self + } + + #[must_use] + pub fn rows(mut self, amount: GridAmount) -> Self { + self.1.push(format!("grid-rows-{}", amount.to_value())); + self + } + + #[must_use] + pub fn auto_flow(mut self, flow: GridAutoFlow) -> Self { + self.1.push(flow.to_value().to_string()); + self + } + + #[must_use] + pub fn auto_columns(mut self, size: GridAutoSize) -> Self { + self.1.push(format!("auto-cols-{}", size.to_value())); + self + } + + #[must_use] + pub fn auto_rows(mut self, size: GridAutoSize) -> Self { + self.1.push(format!("auto-rows-{}", size.to_value())); + self + } + + #[must_use] + pub fn full_center(mut self) -> Self { + self.1.push("items-center".to_owned()); + self.1.push("justify-center".to_owned()); + self + } + + #[must_use] + pub const fn group(mut self) -> Self { + self.2 = true; + self + } + + #[must_use] + pub fn divide_style(mut self, style: DivideStyle) -> Self { + self.1.push(style.to_value().to_string()); + self + } + + #[must_use] + pub fn divide_color(mut self, color: C) -> Self { + self.1.push(format!("divide-{}", color.color_class())); + self + } + + #[must_use] + pub fn divide_x(mut self, width: DivideWidth) -> Self { + self.1.push(format!("divide-x-{}", width.to_value())); + self + } + + #[must_use] + pub fn divide_y(mut self, width: DivideWidth) -> Self { + self.1.push(format!("divide-y-{}", width.to_value())); + self + } + + #[must_use] + pub fn justify_items(mut self, justify: JustifyItems) -> Self { + self.1.push(justify.to_value().to_string()); + self + } + + #[must_use] + pub fn align_content(mut self, align: AlignContent) -> Self { + self.1.push(align.to_value().to_string()); + self + } + + #[must_use] + pub fn align_items(mut self, align: AlignItems) -> Self { + self.1.push(align.to_value().to_string()); + self + } + + #[must_use] + pub fn justify(mut self, value: Justify) -> Self { + let class = match value { + Justify::Center => "justify-center".to_string(), + Justify::Between => "justify-between".to_string(), + Justify::Normal => "justify-normal".to_string(), + Justify::Start => "justify-start".to_string(), + Justify::End => "justify-end".to_string(), + Justify::Around => "justify-around".to_string(), + Justify::Evenly => "justify-evenly".to_string(), + Justify::Stretch => "justify-stretch".to_string(), + }; + + self.1.push(class); + self + } + + #[must_use] + pub fn items_center(mut self) -> Self { + self.1.push("items-center".to_owned()); + self + } + + #[must_use] + pub fn gap(mut self, amount: ScreenValue) -> Self { + self.1.push(format!("gap-{}", amount.to_value())); + self + } + + #[must_use] + pub fn gap_x(mut self, amount: ScreenValue) -> Self { + self.1.push(format!("gap-x-{}", amount.to_value())); + self + } + + #[must_use] + pub fn gap_y(mut self, amount: ScreenValue) -> Self { + self.1.push(format!("gap-y-{}", amount.to_value())); + self + } +} + +impl UIWidget for GridWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let mut res = vec!["grid".to_string()]; + res.extend_from_slice(&self.1); + res + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() && !self.2 { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum GridAutoFlow { + Row, + Column, + Dense, + RowDense, + ColumnDense, +} + +impl GridAutoFlow { + pub const fn to_value(&self) -> &str { + match self { + GridAutoFlow::Row => "grid-flow-row", + GridAutoFlow::Column => "grid-flow-col", + GridAutoFlow::Dense => "grid-flow-dense", + GridAutoFlow::RowDense => "grid-flow-row-dense", + GridAutoFlow::ColumnDense => "grid-flow-col-dense", + } + } +} + +pub enum GridAutoSize { + Auto, + Min, + Max, + Fr, +} + +impl GridAutoSize { + pub const fn to_value(&self) -> &str { + match self { + GridAutoSize::Auto => "auto", + GridAutoSize::Min => "min", + GridAutoSize::Max => "max", + GridAutoSize::Fr => "fr", + } + } +} + +#[allow(non_snake_case)] +pub fn GridElementColumn(inner: T) -> GridElement { + GridElement(Box::new(inner), Vec::new(), "col".to_string()) +} + +#[allow(non_snake_case)] +pub fn GridElementRow(inner: T) -> GridElement { + GridElement(Box::new(inner), Vec::new(), "row".to_string()) +} + +pub struct GridElement(Box, Vec, String); + +impl GridElement { + pub fn auto(mut self) -> Self { + self.1.push(format!("{}-auto", self.2)); + self + } + + pub fn span(mut self, value: GridElementValue) -> Self { + self.1.push(format!("{}-span-{}", self.2, match value { + GridElementValue::_1 => "1", + GridElementValue::_2 => "2", + GridElementValue::_3 => "3", + GridElementValue::_4 => "4", + GridElementValue::_5 => "5", + GridElementValue::_6 => "6", + GridElementValue::_7 => "7", + GridElementValue::_8 => "8", + GridElementValue::_9 => "9", + GridElementValue::_10 => "10", + GridElementValue::_11 => "11", + GridElementValue::_12 => "12", + GridElementValue::Auto => "full", + })); + self + } + + pub fn start(mut self, value: GridElementValue) -> Self { + self.1 + .push(format!("{}-start-{}", self.2, value.to_value())); + self + } + + pub fn end(mut self, value: GridElementValue) -> Self { + self.1.push(format!("{}-end-{}", self.2, value.to_value())); + self + } +} + +impl Render for GridElement { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for GridElement { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let mut res = vec!["grid".to_string()]; + res.extend_from_slice(&self.1); + res + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum GridElementValue { + _1, + _2, + _3, + _4, + _5, + _6, + _7, + _8, + _9, + _10, + _11, + _12, + Auto, +} + +impl GridElementValue { + pub const fn to_value(&self) -> &str { + match self { + GridElementValue::_1 => "1", + GridElementValue::_2 => "2", + GridElementValue::_3 => "3", + GridElementValue::_4 => "4", + GridElementValue::_5 => "5", + GridElementValue::_6 => "6", + GridElementValue::_7 => "7", + GridElementValue::_8 => "8", + GridElementValue::_9 => "9", + GridElementValue::_10 => "10", + GridElementValue::_11 => "11", + GridElementValue::_12 => "12", + GridElementValue::Auto => "auto", + } + } +} + +macro_rules! string_class_widget { + ($name:ident) => { + pub struct $name(Box, String); + + impl Render for $name { + fn render(&self) -> Markup { + self.render_with_class("") + } + } + + impl UIWidget for $name { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![self.1.clone()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } + } + }; +} + +macro_rules! constructor { + ($name:ident, $class:literal) => { + #[allow(non_snake_case)] + pub fn $name(inner: T) -> Self { + Self(Box::new(inner), $class.to_string()) + } + }; +} + +string_class_widget!(Columns); + +impl Columns { + constructor!(_1, "columns-1"); + constructor!(_2, "columns-2"); + constructor!(_3, "columns-3"); + constructor!(_4, "columns-4"); + constructor!(_5, "columns-5"); + constructor!(_6, "columns-6"); + constructor!(_7, "columns-7"); + constructor!(_8, "columns-8"); + constructor!(_9, "columns-9"); + constructor!(_10, "columns-10"); + constructor!(_11, "columns-11"); + constructor!(_12, "columns-12"); + constructor!(Auto, "columns-auto"); + constructor!(_3XS, "columns-3xs"); + constructor!(_2XS, "columns-2xs"); + constructor!(XS, "columns-xs"); + constructor!(Small, "columns-sm"); + constructor!(Medium, "columns-md"); + constructor!(Large, "columns-lg"); + constructor!(XL, "columns-xl"); + constructor!(_2XL, "columns-2xl"); + constructor!(_3XL, "columns-3xl"); + constructor!(_4XL, "columns-4xl"); + constructor!(_5XL, "columns-5xl"); + constructor!(_6XL, "columns-6xl"); + constructor!(_7XL, "columns-7xl"); +} diff --git a/src/ui/primitives/list.rs b/src/ui/primitives/list.rs new file mode 100644 index 0000000..630a766 --- /dev/null +++ b/src/ui/primitives/list.rs @@ -0,0 +1,103 @@ +use crate::ui::UIWidget; +use maud::{Markup, Render, html}; + +#[allow(non_snake_case)] +#[must_use] +pub fn OrderedList() -> ListWidget { + ListWidget(Vec::new(), true) +} + +#[allow(non_snake_case)] +#[must_use] +pub fn UnorderedList() -> ListWidget { + ListWidget(Vec::new(), false) +} + +pub struct ListWidget(Vec>, bool); + +impl ListWidget { + #[must_use] + pub fn push(mut self, element: T) -> Self { + self.0.push(Box::new(element)); + self + } + + #[must_use] + pub fn push_some T>( + mut self, + option: Option, + then: U, + ) -> Self { + if let Some(val) = option { + self.0.push(Box::new(then(val))); + } + self + } + + #[must_use] + pub fn push_if T>( + mut self, + condition: bool, + then: U, + ) -> Self { + if condition { + self.0.push(Box::new(then())); + } + self + } + + #[must_use] + pub fn push_for_each(mut self, items: &[X], mut action: F) -> Self + where + T: UIWidget + 'static, + F: FnMut(&X) -> T, + { + for item in items { + self.0.push(Box::new(action(item))); + } + + self + } +} + +impl Render for ListWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for ListWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![] + } + + fn extended_class(&self) -> Vec { + vec![] + } + + fn render_with_class(&self, class: &str) -> Markup { + let inner = html! { + @for e in &self.0 { + li { (e.as_ref()) }; + } + }; + + if self.1 { + html! { + ol class=(class) { + (inner); + } + } + } else { + html! { + ul class=(class) { + (inner); + } + } + } + } +} diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs index 0e1c096..d2d21f5 100644 --- a/src/ui/primitives/mod.rs +++ b/src/ui/primitives/mod.rs @@ -1,25 +1,33 @@ -use maud::{PreEscaped, html}; - use super::UIWidget; +use maud::{Markup, PreEscaped, Render, html}; pub mod animation; pub mod aspect; pub mod background; +pub mod border; pub mod container; pub mod cursor; +pub mod display; pub mod div; +pub mod filter; pub mod flex; +pub mod grid; pub mod header; pub mod height; pub mod image; pub mod input; pub mod link; +pub mod list; pub mod margin; pub mod padding; +pub mod position; pub mod rounded; +pub mod scroll; pub mod shadow; pub mod sized; pub mod space; +pub mod svg; +pub mod table; pub mod text; pub mod transform; pub mod visibility; @@ -62,6 +70,7 @@ pub fn script(script: &str) -> PreEscaped { } pub enum Size { + Custom(String), None, Small, Regular, @@ -77,6 +86,7 @@ impl Size { #[must_use] pub const fn to_value(&self) -> &str { match self { + Self::Custom(str) => str.as_str(), Self::None => "none", Self::Small => "sm", Self::Regular => "", @@ -130,3 +140,46 @@ impl Side { } } } + +#[allow(non_snake_case)] +pub fn NoBrowserAppearance(inner: T) -> NoBrowserAppearanceWidget { + NoBrowserAppearanceWidget(Box::new(inner)) +} + +pub struct NoBrowserAppearanceWidget(Box); + +impl Render for NoBrowserAppearanceWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for NoBrowserAppearanceWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec!["appearance-none".to_string()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("appearance-none {class}")) + } else { + html! { + div class=(format!("appearance-none {class}")) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/primitives/position.rs b/src/ui/primitives/position.rs new file mode 100644 index 0000000..2019981 --- /dev/null +++ b/src/ui/primitives/position.rs @@ -0,0 +1,341 @@ +use maud::{Markup, Render, html}; + +use crate::ui::UIWidget; + +use super::Side; + +#[allow(non_snake_case)] +pub fn Position(kind: PositionKind, inner: T) -> Positioned { + Positioned { + inner: Box::new(inner), + kind, + inset: None, + inset_x: None, + inset_y: None, + start: None, + end: None, + top: None, + right: None, + bottom: None, + left: None, + } +} + +pub struct Positioned { + inner: Box, + kind: PositionKind, + inset: Option, + inset_x: Option, + inset_y: Option, + start: Option, + end: Option, + top: Option, + right: Option, + bottom: Option, + left: Option, +} + +impl Positioned { + pub fn inset(mut self, value: i64) -> Self { + self.inset = Some(value); + self + } + pub fn inset_x(mut self, value: i64) -> Self { + self.inset_x = Some(value); + self + } + + pub fn inset_y(mut self, value: i64) -> Self { + self.inset_y = Some(value); + self + } + + pub fn start(mut self, value: i64) -> Self { + self.start = Some(value); + self + } + + pub fn end(mut self, value: i64) -> Self { + self.end = Some(value); + self + } + + pub fn top(mut self, value: i64) -> Self { + self.top = Some(value); + self + } + + pub fn right(mut self, value: i64) -> Self { + self.right = Some(value); + self + } + + pub fn bottom(mut self, value: i64) -> Self { + self.bottom = Some(value); + self + } + + pub fn left(mut self, value: i64) -> Self { + self.left = Some(value); + self + } +} + +impl Render for Positioned { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for Positioned { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let mut ret = vec![self.kind.to_value().to_string()]; + + if let Some(inset) = &self.inset { + if inset.is_negative() { + ret.push(format!("-inset-[{inset}px]")); + } else { + ret.push(format!("inset-[{inset}px]")); + } + } + + if let Some(inset) = &self.inset_x { + if inset.is_negative() { + ret.push(format!("-inset-x-[{inset}px]")); + } else { + ret.push(format!("inset-x-[{inset}px]")); + } + } + + if let Some(inset) = &self.inset_y { + if inset.is_negative() { + ret.push(format!("-inset-y-[{inset}px]")); + } else { + ret.push(format!("inset-y-[{inset}px]")); + } + } + + if let Some(start) = &self.start { + if start.is_negative() { + ret.push(format!("-start-[{start}px]")); + } else { + ret.push(format!("start-[{start}px]")); + } + } + + if let Some(end) = &self.end { + if end.is_negative() { + ret.push(format!("-end-[{end}px]")); + } else { + ret.push(format!("end-[{end}px]")); + } + } + + if let Some(value) = &self.top { + if value.is_negative() { + ret.push(format!("-top-[{value}px]")); + } else { + ret.push(format!("top-[{value}px]")); + } + } + + if let Some(value) = &self.right { + if value.is_negative() { + ret.push(format!("-right-[{value}px]")); + } else { + ret.push(format!("right-[{value}px]")); + } + } + + if let Some(value) = &self.bottom { + if value.is_negative() { + ret.push(format!("-bottom-[{value}px]")); + } else { + ret.push(format!("bottom-[{value}px]")); + } + } + + if let Some(value) = &self.left { + if value.is_negative() { + ret.push(format!("-left-[{value}px]")); + } else { + ret.push(format!("left-[{value}px]")); + } + } + + ret + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.inner.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.inner.as_ref().can_inherit() { + self.inner + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.inner.as_ref()) + } + } + } + } +} + +pub enum PositionKind { + Static, + Fixed, + Absolute, + Relative, + Sticky, +} + +impl PositionKind { + pub const fn to_value(&self) -> &str { + match self { + PositionKind::Static => "static", + PositionKind::Fixed => "fixed", + PositionKind::Absolute => "absolute", + PositionKind::Relative => "relative", + PositionKind::Sticky => "sticky", + } + } +} + +#[allow(non_snake_case)] +pub fn ObjectPosition(side: Side, inner: T) -> ObjectPositioned { + ObjectPositioned { + inner: Box::new(inner), + side, + } +} + +pub struct ObjectPositioned { + inner: Box, + side: Side, +} + +impl Render for ObjectPositioned { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for ObjectPositioned { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![ + match self.side { + Side::Start => "object-top", + Side::End => "object-bottom", + Side::Top => "object-top", + Side::Right => "object-right", + Side::Bottom => "object-bottom", + Side::Left => "object-left", + Side::StartStart => "object-left-top", + Side::StartEnd => "object-right-top", + Side::EndEnd => "object-right-bottom", + Side::EndStart => "object-left-bottom", + Side::TopLeft => "object-left-top", + Side::TopRight => "object-right-top", + Side::BottomRight => "object-right-bottom", + Side::BottomLeft => "object-left-bottom", + Side::Center => "object-center", + } + .to_string(), + ] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.inner.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.inner.as_ref().can_inherit() { + self.inner + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.inner.as_ref()) + } + } + } + } +} + +pub enum Resize { + None, + Y, + X, + Both, +} + +impl Resize { + pub const fn to_value(&self) -> &str { + match self { + Resize::None => "resize-none", + Resize::Y => "resize-y", + Resize::X => "resize-x", + Resize::Both => "resize", + } + } +} + +#[allow(non_snake_case)] +pub fn Resizeable(mode: Resize, inner: T) -> ResizeableWidget { + ResizeableWidget(Box::new(inner), mode) +} + +pub struct ResizeableWidget(Box, Resize); + +impl Render for ResizeableWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for ResizeableWidget { + fn can_inherit(&self) -> bool { + false + } + + fn base_class(&self) -> Vec { + vec![self.1.to_value().to_string()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/primitives/rounded.rs b/src/ui/primitives/rounded.rs index f716814..d448fe3 100644 --- a/src/ui/primitives/rounded.rs +++ b/src/ui/primitives/rounded.rs @@ -12,7 +12,7 @@ pub struct RoundedWidget(Box, Option, Option); impl RoundedWidget { #[must_use] - pub const fn size(mut self, size: Size) -> Self { + pub fn size(mut self, size: Size) -> Self { self.1 = Some(size); self } diff --git a/src/ui/primitives/scroll.rs b/src/ui/primitives/scroll.rs new file mode 100644 index 0000000..720ffe2 --- /dev/null +++ b/src/ui/primitives/scroll.rs @@ -0,0 +1,234 @@ +use super::{margin::Margin, padding::PaddingWidget}; +use crate::ui::UIWidget; +use maud::{Markup, Render, html}; + +#[allow(non_snake_case)] +pub fn Scroll(inner: T) -> ScrollWidget { + ScrollWidget(Box::new(inner), true, None, None, None, None, false) +} + +pub struct ScrollWidget( + Box, + bool, + Option, + Option, + Option, + Option, + bool, +); + +impl ScrollWidget { + pub fn smooth(mut self, value: bool) -> Self { + self.1 = value; + self + } + + pub fn scroll_margin(mut self, margin: Margin) -> Self { + self.2 = Some(margin); + self + } + + pub fn scroll_padding(mut self, padding: PaddingWidget) -> Self { + self.3 = Some(padding); + self + } + + pub fn overscroll(mut self, behaviour: Overscroll) -> Self { + self.4 = Some(behaviour); + self + } + + pub fn snap(mut self, kind: SnapType) -> Self { + self.5 = Some(kind); + self + } + + pub fn skip_snap(mut self) -> Self { + self.6 = true; + self + } +} + +impl Render for ScrollWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for ScrollWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + let mut ret = Vec::new(); + + if self.1 { + ret.push("scroll-smooth".to_string()); + } + + if let Some(margin) = &self.2 { + let classes = margin + .base_class() + .into_iter() + .map(|x| format!("scroll-{x}")) + .collect::>(); + ret.extend_from_slice(&classes); + } + + if let Some(padding) = &self.3 { + let classes = padding + .base_class() + .into_iter() + .map(|x| format!("scroll-{x}")) + .collect::>(); + ret.extend_from_slice(&classes); + } + + if let Some(overscroll) = &self.4 { + ret.push(overscroll.to_value().to_string()); + } + + if let Some(snap) = &self.5 { + ret.push(snap.to_value().to_string()); + } + + if self.6 { + ret.push("snap-normal".to_string()); + } else { + ret.push("snap-always".to_string()); + } + + ret + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} + +pub enum Overscroll { + Auto, + Contain, + None, + YAuto, + YContain, + YNone, + XAuto, + XContain, + XNone, +} + +impl Overscroll { + pub const fn to_value(&self) -> &str { + match self { + Overscroll::Auto => "overscroll-auto", + Overscroll::Contain => "overscroll-contain", + Overscroll::None => "overscroll-none", + Overscroll::YAuto => "overscroll-y-auto", + Overscroll::YContain => "overscroll-y-contain", + Overscroll::YNone => "overscroll-y-none", + Overscroll::XAuto => "overscroll-x-auto", + Overscroll::XContain => "overscroll-x-contain", + Overscroll::XNone => "overscroll-x-none", + } + } +} + +pub enum SnapType { + None, + X, + Y, + Both, + Mandatory, + Proximity, +} + +impl SnapType { + pub const fn to_value(&self) -> &str { + match self { + SnapType::None => "snap-none", + SnapType::X => "snap-x", + SnapType::Y => "snap-y", + SnapType::Both => "snap-both", + SnapType::Mandatory => "snap-mandatory", + SnapType::Proximity => "snap-proximity", + } + } +} + +pub struct SnapAlign(Box, String); + +impl SnapAlign { + #[allow(non_snake_case)] + pub fn Start(inner: T) -> Self { + Self(Box::new(inner), "snap-start".to_string()) + } + + #[allow(non_snake_case)] + pub fn End(inner: T) -> Self { + Self(Box::new(inner), "snap-end".to_string()) + } + + #[allow(non_snake_case)] + pub fn Center(inner: T) -> Self { + Self(Box::new(inner), "snap-center".to_string()) + } + + #[allow(non_snake_case)] + pub fn None(inner: T) -> Self { + Self(Box::new(inner), "snap-align-none".to_string()) + } +} + +impl Render for SnapAlign { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for SnapAlign { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + vec![self.1.clone()] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/primitives/shadow.rs b/src/ui/primitives/shadow.rs index 92f2323..5e3cc64 100644 --- a/src/ui/primitives/shadow.rs +++ b/src/ui/primitives/shadow.rs @@ -1,35 +1,46 @@ -use crate::ui::UIWidget; +use crate::ui::{UIWidget, color::UIColor}; use maud::{Markup, Render, html}; -pub struct Shadow(Box, String); +pub struct Shadow(Box, String, Option>); impl Shadow { - pub fn medium(inner: T) -> Self { - Self(Box::new(inner), "md".to_owned()) - } - pub fn small(inner: T) -> Self { - Self(Box::new(inner), "sm".to_owned()) + Self(Box::new(inner), "sm".to_owned(), None) } pub fn regular(inner: T) -> Self { - Self(Box::new(inner), String::new()) + Self(Box::new(inner), String::new(), None) + } + + pub fn medium(inner: T) -> Self { + Self(Box::new(inner), "md".to_owned(), None) } pub fn large(inner: T) -> Self { - Self(Box::new(inner), "lg".to_owned()) - } - - pub fn none(inner: T) -> Self { - Self(Box::new(inner), "none".to_owned()) + Self(Box::new(inner), "lg".to_owned(), None) } pub fn xl(inner: T) -> Self { - Self(Box::new(inner), "xl".to_owned()) + Self(Box::new(inner), "xl".to_owned(), None) } pub fn _2xl(inner: T) -> Self { - Self(Box::new(inner), "2xl".to_owned()) + Self(Box::new(inner), "2xl".to_owned(), None) + } + + pub fn inner(inner: T) -> Self { + Self(Box::new(inner), "inner".to_owned(), None) + } + + pub fn none(inner: T) -> Self { + Self(Box::new(inner), "none".to_owned(), None) + } +} + +impl Shadow { + pub fn color(mut self, color: C) -> Self { + self.2 = Some(Box::new(color)); + self } } @@ -45,11 +56,17 @@ impl UIWidget for Shadow { } fn base_class(&self) -> Vec { - if self.1.is_empty() { + let mut ret = if self.1.is_empty() { vec!["shadow".to_string()] } else { vec![format!("shadow-{}", self.1)] + }; + + if let Some(color) = &self.2 { + ret.push(format!("shadow-{}", color.color_class())); } + + ret } fn extended_class(&self) -> Vec { diff --git a/src/ui/primitives/svg.rs b/src/ui/primitives/svg.rs new file mode 100644 index 0000000..79de1cf --- /dev/null +++ b/src/ui/primitives/svg.rs @@ -0,0 +1,76 @@ +use maud::{Markup, Render, html}; + +use crate::ui::{UIWidget, color::UIColor}; + +#[allow(non_snake_case)] +pub fn SVG(inner: T) -> SVGWidget { + SVGWidget(Box::new(inner), None, None, None) +} + +pub struct SVGWidget( + Box, + Option>, + Option>, + Option, +); + +impl SVGWidget { + pub fn fill(mut self, color: C) -> Self { + self.1 = Some(Box::new(color)); + self + } + + pub fn stroke(mut self, color: C) -> Self { + self.2 = Some(Box::new(color)); + self + } + + pub fn stroke_width(mut self, width: u32) -> Self { + self.3 = Some(width); + self + } +} + +impl Render for SVGWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for SVGWidget { + fn can_inherit(&self) -> bool { + false + } + + fn base_class(&self) -> Vec { + let mut ret = vec![]; + + if let Some(fill) = &self.1 { + ret.push(format!("fill-{}", fill.color_class())); + } + + if let Some(stroke) = &self.2 { + ret.push(format!("stroke-{}", stroke.color_class())); + } + + if let Some(stroke_width) = &self.3 { + ret.push(format!("stroke-[{stroke_width}px]")); + } + + ret + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, _: &str) -> Markup { + html! { + svg class=(self.base_class().join(" ")) { + (self.0.as_ref()) + } + } + } +} diff --git a/src/ui/primitives/table.rs b/src/ui/primitives/table.rs new file mode 100644 index 0000000..60bf8a6 --- /dev/null +++ b/src/ui/primitives/table.rs @@ -0,0 +1,204 @@ +use maud::{Markup, Render, html}; + +use crate::ui::UIWidget; + +use super::{div::Div, space::ScreenValue}; + +#[allow(non_snake_case)] +pub fn Table(inner: Vec>) -> TableWidget { + let inner = Div().vanish().push_for_each(&inner, |row| { + TableRow( + Div() + .vanish() + .push_for_each(&row, |col| TableData(col.clone())), + ) + }); + + TableWidget(Box::new(inner), Vec::new(), None, None) +} + +pub struct TableWidget( + Box, + Vec, + Option>, + Option, +); + +impl TableWidget { + pub fn header(mut self, header: T) -> Self { + self.2 = Some(Box::new(header)); + self + } + + pub fn caption(mut self, caption: Caption) -> Self { + self.3 = Some(caption); + self + } + + pub fn border_collapse(mut self) -> Self { + self.1.push("border-collapse".to_string()); + self + } + + pub fn border_seperate(mut self) -> Self { + self.1.push("border-separate".to_string()); + self + } + + pub fn border_spacing(mut self, spacing: ScreenValue) -> Self { + self.1 + .push(format!("border-spacing-{}", spacing.to_value())); + self + } + + pub fn border_spacing_x(mut self, spacing: ScreenValue) -> Self { + self.1 + .push(format!("border-spacing-x-{}", spacing.to_value())); + self + } + + pub fn border_spacing_y(mut self, spacing: ScreenValue) -> Self { + self.1 + .push(format!("border-spacing-y-{}", spacing.to_value())); + self + } + + pub fn layout_fixed(mut self) -> Self { + self.1.push("table-fixed".to_string()); + self + } + + pub fn layout_auto(mut self) -> Self { + self.1.push("table-auto".to_string()); + self + } +} + +impl Render for TableWidget { + fn render(&self) -> maud::Markup { + self.render_with_class("") + } +} + +impl UIWidget for TableWidget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + self.1.clone() + } + + fn extended_class(&self) -> Vec { + self.base_class() + } + + fn render_with_class(&self, class: &str) -> maud::Markup { + html! { + table class=(format!("{} {class}", self.base_class().join(" "))) { + @if let Some(caption) = &self.3 { + (caption) + } + + @if let Some(header) = &self.2 { + thead { + (header) + }; + }; + + (self.0.as_ref()) + }; + } + } +} + +pub struct Caption(Box, bool); + +impl Caption { + #[allow(non_snake_case)] + pub fn Top(inner: T) -> Self { + Self(Box::new(inner), true) + } + + #[allow(non_snake_case)] + pub fn Bottom(inner: T) -> Self { + Self(Box::new(inner), false) + } +} + +impl Render for Caption { + fn render(&self) -> maud::Markup { + self.render_with_class("") + } +} + +impl UIWidget for Caption { + fn can_inherit(&self) -> bool { + false + } + + fn base_class(&self) -> Vec { + if self.1 { + vec!["caption-top".to_string()] + } else { + vec!["caption-bottom".to_string()] + } + } + + fn extended_class(&self) -> Vec { + self.base_class() + } + + fn render_with_class(&self, _: &str) -> maud::Markup { + html! { + caption class=(self.base_class().join(" ")) { + (self.0.as_ref()) + }; + } + } +} + +macro_rules! element_widget { + ($name:ident, $widget:ident, $element:ident) => { + #[allow(non_snake_case)] + pub fn $name(inner: T) -> $widget { + $widget(Box::new(inner)) + } + + pub struct $widget(Box); + + impl Render for $widget { + fn render(&self) -> Markup { + self.render_with_class("") + } + } + + impl UIWidget for $widget { + fn can_inherit(&self) -> bool { + true + } + + fn base_class(&self) -> Vec { + Vec::new() + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + html! { + $element class=(class) { + (self.0.as_ref()) + } + } + } + } + }; +} + +element_widget!(TableRow, TableRowWidget, tr); +element_widget!(TableHead, TableHeadWidget, th); +element_widget!(TableData, TableDataWidget, td); diff --git a/src/ui/primitives/text.rs b/src/ui/primitives/text.rs index 5230a1f..43befe7 100644 --- a/src/ui/primitives/text.rs +++ b/src/ui/primitives/text.rs @@ -1,5 +1,5 @@ use crate::ui::{UIWidget, color::UIColor}; -use maud::{Markup, PreEscaped, Render}; +use maud::{Markup, PreEscaped, Render, html}; use super::{Nothing, space::ScreenValue}; @@ -31,6 +31,7 @@ pub fn Text(txt: &str) -> TextWidget { align: None, vert_align: None, list_style: None, + select: None, kind: TextKind::Paragraph, } } @@ -68,6 +69,7 @@ pub fn Paragraph(inner: T) -> TextWidget { list_style: None, clamp: None, align: None, + select: None, kind: TextKind::Paragraph, } } @@ -100,6 +102,7 @@ pub fn Span(txt: &str) -> TextWidget { clamp: None, align: None, pseudo: None, + select: None, kind: TextKind::Span, } } @@ -132,6 +135,7 @@ pub fn Code(txt: &str) -> TextWidget { clamp: None, align: None, pseudo: None, + select: None, kind: TextKind::Pre, } } @@ -160,10 +164,17 @@ pub struct TextWidget { pseudo: Option, align: Option, clamp: Option, + select: Option, title: Option, } impl TextWidget { + #[must_use] + pub fn select(mut self, select: TextSelection) -> Self { + self.select = Some(select); + self + } + #[must_use] pub fn whitespace(mut self, whitespace: TextWhitespace) -> Self { self.whitespace = Some(whitespace); @@ -527,6 +538,7 @@ impl UIWidget for TextWidget { add_option!(list_style, ret); add_option!(pseudo, ret); add_option!(line_height, ret); + add_option!(select, ret); if let Some(decoration) = &self.decoration { ret.extend_from_slice(&decoration.base_class()); @@ -1061,3 +1073,73 @@ impl TextHyphens { } } } + +pub enum TextSelection { + None, + Text, + All, + Auto, +} + +impl TextSelection { + pub const fn to_value(&self) -> &str { + match self { + TextSelection::None => "select-none", + TextSelection::Text => "select-text", + TextSelection::All => "select-all", + TextSelection::Auto => "select-auto ", + } + } +} + +macro_rules! color_widget { + ($constr:ident, $widget:ident, $class:literal) => { + #[allow(non_snake_case)] + pub fn $constr(color: C, inner: T) -> $widget { + $widget(Box::new(inner), Box::new(color)) + } + + pub struct $widget(Box, Box); + + impl Render for $widget { + fn render(&self) -> Markup { + self.render_with_class("") + } + } + + impl UIWidget for $widget { + fn can_inherit(&self) -> bool { + false + } + + fn base_class(&self) -> Vec { + let mut class = $class.to_string(); + class.push_str(&format!("-{}", self.1.color_class())); + vec![class] + } + + fn extended_class(&self) -> Vec { + let mut c = self.base_class(); + c.extend_from_slice(&self.0.extended_class()); + c + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("{} {class}", self.base_class().join(" "))) + } else { + html! { + div class=(format!("{} {class}", self.base_class().join(" "))) { + (self.0.as_ref()) + } + } + } + } + } + }; +} + +color_widget!(TextCursorColor, CaretColorWidget, "caret"); +color_widget!(AccentColor, AccentColorWidget, "accent"); diff --git a/src/ui/wrapper/mod.rs b/src/ui/wrapper/mod.rs index 4fac146..06654a3 100644 --- a/src/ui/wrapper/mod.rs +++ b/src/ui/wrapper/mod.rs @@ -74,6 +74,29 @@ macro_rules! wrapper { } wrapper!(Hover, HoverWrapper, "hover"); +wrapper!(DarkMode, DarkModeWrapper, "dark"); +wrapper!(Active, ActiveWrapper, "active"); +wrapper!(Focus, FocusWrapper, "focus"); +wrapper!(First, FirstWrapper, "first"); +wrapper!(Odd, OddWrapper, "odd"); +wrapper!(Even, EvenWrapper, "even"); + +wrapper!(Required, RequiredWrapper, "required"); +wrapper!(Invalid, InvalidWrapper, "invalid"); +wrapper!(Disabled, DisabledWrapper, "disabled"); +wrapper!(Placeholder, PlaceholderWrapper, "placeholder"); +wrapper!(FileButton, FileButtonWrapper, "file"); +wrapper!(Marker, MarkerWrapper, "marker"); +wrapper!(Selection, SelectionWrapper, "selection"); +wrapper!(FirstLine, FirstLineWrapper, "first-line"); +wrapper!(FirstLetter, FirstLetterWrapper, "first-letter"); + +wrapper!(Portrait, PortraitWrapper, "portrait"); +wrapper!(Landscape, LandscapeWrapper, "landscape"); +wrapper!(Print, PrintWrapper, "print"); +wrapper!(LeftToRight, LeftToRightWrapper, "ltr"); +wrapper!(RightToLeft, RightToLeftWrapper, "rtl"); +wrapper!(Opened, OpenWrapper, "open"); wrapper!(SmallScreen, SmallScreenWrapper, "sm"); wrapper!(MediumScreen, MediumScreenWrapper, "md");