use peniko::Color;
use crate::tokens::{FamilyRole, TextSize, Weight};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Length {
Px(f32),
Pct(f32),
Ch(f32),
#[default]
Auto,
}
impl Length {
pub(crate) fn resolved(self, ch_px: f32) -> Length {
match self {
Length::Ch(n) => Length::Px(n * ch_px),
other => other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Display {
#[default]
Flex,
Grid,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
Row,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignItems {
#[default]
Stretch,
Start,
Center,
End,
Baseline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JustifyContent {
#[default]
Start,
Center,
End,
SpaceBetween,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignContent {
#[default]
Start,
Center,
End,
Stretch,
SpaceBetween,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
#[default]
Relative,
Absolute,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default]
Visible,
Hidden,
Scroll,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Edges {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Edges {
pub const fn all(v: f32) -> Self {
Self {
top: v,
right: v,
bottom: v,
left: v,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Inset {
pub top: Option<f32>,
pub right: Option<f32>,
pub bottom: Option<f32>,
pub left: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Track {
Px(f32),
Fr(f32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct GridPlace {
pub start: Option<i16>,
pub span: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GradientStop {
pub offset: f32,
pub color: Color,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Paint {
Solid(Color),
LinearGradient {
angle_deg: f32,
stops: Vec<GradientStop>,
},
RadialGradient {
center: (f32, f32),
radius: f32,
stops: Vec<GradientStop>,
},
}
impl From<Color> for Paint {
fn from(c: Color) -> Self {
Self::Solid(c)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Border {
pub width: f32,
pub color: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CornerRadius {
pub tl: f32,
pub tr: f32,
pub br: f32,
pub bl: f32,
}
impl CornerRadius {
pub const fn all(r: f32) -> Self {
Self {
tl: r,
tr: r,
br: r,
bl: r,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Shadow {
pub dx: f32,
pub dy: f32,
pub blur: f32,
pub spread: f32,
pub color: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextAlign {
#[default]
Start,
Center,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FigureStyle {
#[default]
Default,
Lining,
OldStyle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum NumericSpacing {
#[default]
Default,
Proportional,
Tabular,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FontFeatures {
pub figures: FigureStyle,
pub spacing: NumericSpacing,
pub small_caps: bool,
pub ligatures: Option<bool>,
pub fractions: bool,
}
impl FontFeatures {
pub(crate) fn feature_string(&self) -> Option<String> {
let mut parts: Vec<&'static str> = Vec::new();
match self.figures {
FigureStyle::Default => {}
FigureStyle::Lining => parts.push("\"lnum\" 1"),
FigureStyle::OldStyle => parts.push("\"onum\" 1"),
}
match self.spacing {
NumericSpacing::Default => {}
NumericSpacing::Proportional => parts.push("\"pnum\" 1"),
NumericSpacing::Tabular => parts.push("\"tnum\" 1"),
}
if self.small_caps {
parts.push("\"smcp\" 1");
}
match self.ligatures {
None => {}
Some(true) => parts.push("\"liga\" 1"),
Some(false) => parts.push("\"liga\" 0"),
}
if self.fractions {
parts.push("\"frac\" 1");
}
if parts.is_empty() {
None
} else {
Some(parts.join(", "))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct TextStyle {
pub size: TextSize,
pub size_px: Option<f32>,
pub weight: Weight,
pub color: Option<Color>,
pub line_height: Option<f32>,
pub letter_spacing: Option<f32>,
pub family: FamilyRole,
pub align: TextAlign,
pub max_lines: Option<u32>,
pub features: FontFeatures,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Style {
pub display: Display,
pub direction: Direction,
pub wrap: bool,
pub align_items: AlignItems,
pub align_self: Option<AlignItems>,
pub justify_content: JustifyContent,
pub align_content: AlignContent,
pub gap: f32,
pub padding: Edges,
pub margin: Edges,
pub inset: Inset,
pub position: Position,
pub width: Length,
pub height: Length,
pub min_width: Length,
pub max_width: Length,
pub min_height: Length,
pub max_height: Length,
pub flex_grow: f32,
pub flex_shrink: f32,
pub flex_basis: Length,
pub grid_template_columns: Vec<Track>,
pub grid_template_rows: Vec<Track>,
pub grid_column: GridPlace,
pub grid_row: GridPlace,
pub overflow_x: Overflow,
pub overflow_y: Overflow,
pub fill: Option<Paint>,
pub border: Option<Border>,
pub corner_radius: CornerRadius,
pub shadow_token: Option<crate::tokens::ShadowToken>,
pub shadows: Vec<Shadow>,
pub highlight_top: Option<Color>,
pub opacity: f32,
pub scale: f32,
pub clip: bool,
pub path_trim: f32,
pub text: TextStyle,
}
impl Default for Style {
fn default() -> Self {
Self {
display: Display::default(),
direction: Direction::default(),
wrap: false,
align_items: AlignItems::default(),
align_self: None,
justify_content: JustifyContent::default(),
align_content: AlignContent::default(),
gap: 0.0,
padding: Edges::default(),
margin: Edges::default(),
inset: Inset::default(),
position: Position::default(),
width: Length::Auto,
height: Length::Auto,
min_width: Length::Auto,
max_width: Length::Auto,
min_height: Length::Auto,
max_height: Length::Auto,
flex_grow: 0.0,
flex_shrink: 1.0,
flex_basis: Length::Auto,
grid_template_columns: Vec::new(),
grid_template_rows: Vec::new(),
grid_column: GridPlace::default(),
grid_row: GridPlace::default(),
overflow_x: Overflow::Visible,
overflow_y: Overflow::Visible,
fill: None,
border: None,
corner_radius: CornerRadius::default(),
shadow_token: None,
shadows: Vec::new(),
highlight_top: None,
opacity: 1.0,
scale: 1.0,
clip: false,
path_trim: 1.0,
text: TextStyle::default(),
}
}
}
pub type ThemedFn = Box<dyn Fn(&crate::theme::Theme, Style) -> Style>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpringSpec {
pub stiffness: f32,
pub damping: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transition {
pub colors: bool,
pub opacity: bool,
pub lengths: bool,
pub offsets: bool,
pub shadows: bool,
pub duration_ms: f32,
pub easing: crate::tokens::CubicBezier,
pub spring: Option<SpringSpec>,
}
pub struct Keyframes {
pub(crate) stops: Vec<(f32, ThemedFn)>,
pub(crate) duration_ms: f32,
pub(crate) easing: crate::tokens::CubicBezier,
}
impl Keyframes {
pub fn new(duration_ms: f32) -> Self {
Self {
stops: Vec::new(),
duration_ms,
easing: crate::tokens::EASE_STANDARD,
}
}
pub fn stop(self, at: f32, f: impl Fn(Style) -> Style + 'static) -> Self {
self.themed_stop(at, move |_, s| f(s))
}
pub fn themed_stop(
mut self,
at: f32,
f: impl Fn(&crate::theme::Theme, Style) -> Style + 'static,
) -> Self {
self.stops.push((at.clamp(0.0, 1.0), Box::new(f)));
self.stops.sort_by(|a, b| a.0.total_cmp(&b.0));
self
}
pub fn ease(mut self, easing: crate::tokens::CubicBezier) -> Self {
self.easing = easing;
self
}
}
impl Transition {
pub fn colors() -> Self {
Self {
colors: true,
opacity: false,
lengths: false,
offsets: false,
shadows: true,
duration_ms: crate::tokens::MotionDuration::Fast.ms(),
easing: crate::tokens::EASE_STANDARD,
spring: None,
}
}
pub fn all() -> Self {
Self {
colors: true,
opacity: true,
lengths: true,
offsets: true,
shadows: true,
duration_ms: crate::tokens::MotionDuration::Base.ms(),
easing: crate::tokens::EASE_STANDARD,
spring: None,
}
}
pub fn spring() -> Self {
Self {
spring: Some(SpringSpec {
stiffness: 380.0,
damping: 26.0,
}),
..Self::all()
}
}
pub fn with_spring(mut self, stiffness: f32, damping: f32) -> Self {
self.spring = Some(SpringSpec { stiffness, damping });
self
}
pub fn duration(mut self, d: crate::tokens::MotionDuration) -> Self {
self.duration_ms = d.ms();
self
}
pub fn duration_ms(mut self, ms: f32) -> Self {
self.duration_ms = ms;
self
}
pub fn lengths(mut self, on: bool) -> Self {
self.lengths = on;
self
}
pub fn offsets(mut self, on: bool) -> Self {
self.offsets = on;
self
}
pub fn opacity(mut self, on: bool) -> Self {
self.opacity = on;
self
}
pub fn easing(mut self, e: crate::tokens::CubicBezier) -> Self {
self.easing = e;
self
}
}
impl From<f32> for Length {
fn from(v: f32) -> Self {
Self::Px(v)
}
}
impl Style {
pub fn p(mut self, v: f32) -> Self {
self.padding = Edges::all(v);
self
}
pub fn px(mut self, v: f32) -> Self {
self.padding.left = v;
self.padding.right = v;
self
}
pub fn py(mut self, v: f32) -> Self {
self.padding.top = v;
self.padding.bottom = v;
self
}
pub fn pt(mut self, v: f32) -> Self {
self.padding.top = v;
self
}
pub fn pr(mut self, v: f32) -> Self {
self.padding.right = v;
self
}
pub fn pb(mut self, v: f32) -> Self {
self.padding.bottom = v;
self
}
pub fn pl(mut self, v: f32) -> Self {
self.padding.left = v;
self
}
pub fn m(mut self, v: f32) -> Self {
self.margin = Edges::all(v);
self
}
pub fn mx(mut self, v: f32) -> Self {
self.margin.left = v;
self.margin.right = v;
self
}
pub fn my(mut self, v: f32) -> Self {
self.margin.top = v;
self.margin.bottom = v;
self
}
pub fn mt(mut self, v: f32) -> Self {
self.margin.top = v;
self
}
pub fn mr(mut self, v: f32) -> Self {
self.margin.right = v;
self
}
pub fn mb(mut self, v: f32) -> Self {
self.margin.bottom = v;
self
}
pub fn ml(mut self, v: f32) -> Self {
self.margin.left = v;
self
}
pub fn gap(mut self, v: f32) -> Self {
self.gap = v;
self
}
pub fn w(mut self, v: impl Into<Length>) -> Self {
self.width = v.into();
self
}
pub fn h(mut self, v: impl Into<Length>) -> Self {
self.height = v.into();
self
}
pub fn min_w(mut self, v: impl Into<Length>) -> Self {
self.min_width = v.into();
self
}
pub fn max_w(mut self, v: impl Into<Length>) -> Self {
self.max_width = v.into();
self
}
pub fn min_h(mut self, v: impl Into<Length>) -> Self {
self.min_height = v.into();
self
}
pub fn max_h(mut self, v: impl Into<Length>) -> Self {
self.max_height = v.into();
self
}
pub fn w_full(mut self) -> Self {
self.width = Length::Pct(100.0);
self
}
pub fn h_full(mut self) -> Self {
self.height = Length::Pct(100.0);
self
}
pub fn measure(mut self, chars: f32) -> Self {
self.max_width = Length::Ch(chars);
self
}
pub fn w_ch(mut self, chars: f32) -> Self {
self.width = Length::Ch(chars);
self
}
pub fn min_w_ch(mut self, chars: f32) -> Self {
self.min_width = Length::Ch(chars);
self
}
pub fn max_w_ch(mut self, chars: f32) -> Self {
self.max_width = Length::Ch(chars);
self
}
pub fn grow(mut self) -> Self {
self.flex_grow = 1.0;
self
}
pub fn shrink0(mut self) -> Self {
self.flex_shrink = 0.0;
self
}
pub fn items_start(mut self) -> Self {
self.align_items = AlignItems::Start;
self
}
pub fn items_center(mut self) -> Self {
self.align_items = AlignItems::Center;
self
}
pub fn items_end(mut self) -> Self {
self.align_items = AlignItems::End;
self
}
pub fn items_baseline(mut self) -> Self {
self.align_items = AlignItems::Baseline;
self
}
pub fn justify_start(mut self) -> Self {
self.justify_content = JustifyContent::Start;
self
}
pub fn justify_center(mut self) -> Self {
self.justify_content = JustifyContent::Center;
self
}
pub fn justify_end(mut self) -> Self {
self.justify_content = JustifyContent::End;
self
}
pub fn justify_between(mut self) -> Self {
self.justify_content = JustifyContent::SpaceBetween;
self
}
pub fn wrap(mut self) -> Self {
self.wrap = true;
self
}
pub fn absolute(mut self) -> Self {
self.position = Position::Absolute;
self
}
pub fn top(mut self, v: f32) -> Self {
self.inset.top = Some(v);
self
}
pub fn right(mut self, v: f32) -> Self {
self.inset.right = Some(v);
self
}
pub fn bottom(mut self, v: f32) -> Self {
self.inset.bottom = Some(v);
self
}
pub fn left(mut self, v: f32) -> Self {
self.inset.left = Some(v);
self
}
pub fn overflow_hidden(mut self) -> Self {
self.overflow_x = Overflow::Hidden;
self.overflow_y = Overflow::Hidden;
self.clip = true;
self
}
pub fn scroll_y(mut self) -> Self {
self.overflow_y = Overflow::Scroll;
self.clip = true;
self
}
pub fn bg(mut self, paint: impl Into<Paint>) -> Self {
self.fill = Some(paint.into());
self
}
pub fn border(mut self, width: f32, color: Color) -> Self {
self.border = Some(Border { width, color });
self
}
pub fn rounded(mut self, r: f32) -> Self {
self.corner_radius = CornerRadius::all(r);
self
}
pub fn rounded_full(mut self) -> Self {
self.corner_radius = CornerRadius::all(crate::tokens::R_FULL);
self
}
pub fn shadow(mut self, token: crate::tokens::ShadowToken) -> Self {
self.shadow_token = Some(token);
self
}
pub fn highlight_top(mut self, color: Color) -> Self {
self.highlight_top = Some(color);
self
}
pub fn opacity(mut self, v: f32) -> Self {
self.opacity = v;
self
}
pub fn scale(mut self, v: f32) -> Self {
self.scale = v;
self
}
pub fn trim(mut self, v: f32) -> Self {
self.path_trim = v.clamp(0.0, 1.0);
self
}
pub fn size_px(mut self, px: f32) -> Self {
self.text.size_px = Some(px);
self
}
pub fn tracking(mut self, em: f32) -> Self {
self.text.letter_spacing = Some(em);
self
}
pub fn leading(mut self, multiple: f32) -> Self {
self.text.line_height = Some(multiple);
self
}
pub fn tabular(mut self) -> Self {
self.text.features.spacing = NumericSpacing::Tabular;
self
}
pub fn proportional_nums(mut self) -> Self {
self.text.features.spacing = NumericSpacing::Proportional;
self
}
pub fn oldstyle_nums(mut self) -> Self {
self.text.features.figures = FigureStyle::OldStyle;
self
}
pub fn lining_nums(mut self) -> Self {
self.text.features.figures = FigureStyle::Lining;
self
}
pub fn small_caps(mut self) -> Self {
self.text.features.small_caps = true;
self
}
pub fn ligatures(mut self, on: bool) -> Self {
self.text.features.ligatures = Some(on);
self
}
pub fn fractions(mut self) -> Self {
self.text.features.fractions = true;
self
}
pub fn family(mut self, family: crate::tokens::FamilyRole) -> Self {
self.text.family = family;
self
}
pub fn size(mut self, size: crate::tokens::TextSize) -> Self {
self.text.size = size;
self
}
pub fn weight(mut self, weight: crate::tokens::Weight) -> Self {
self.text.weight = weight;
self
}
pub fn color(mut self, color: Color) -> Self {
self.text.color = Some(color);
self
}
pub fn mono(mut self) -> Self {
self.text.family = crate::tokens::FamilyRole::Mono;
self
}
pub fn truncate(mut self) -> Self {
self.text.max_lines = Some(1);
self
}
pub fn text_align(mut self, align: TextAlign) -> Self {
self.text.align = align;
self
}
}
impl Style {
pub fn grid_cols(mut self, tracks: impl IntoIterator<Item = Track>) -> Self {
self.display = Display::Grid;
self.grid_template_columns = tracks.into_iter().collect();
self
}
pub fn grid_rows(mut self, tracks: impl IntoIterator<Item = Track>) -> Self {
self.display = Display::Grid;
self.grid_template_rows = tracks.into_iter().collect();
self
}
pub fn grid_col(mut self, start: i16, span: u16) -> Self {
self.grid_column = GridPlace {
start: Some(start),
span: (span > 1).then_some(span),
};
self
}
pub fn grid_row(mut self, start: i16, span: u16) -> Self {
self.grid_row = GridPlace {
start: Some(start),
span: (span > 1).then_some(span),
};
self
}
}
impl Style {
pub(crate) fn has_ch(&self) -> bool {
matches!(self.width, Length::Ch(_))
|| matches!(self.min_width, Length::Ch(_))
|| matches!(self.max_width, Length::Ch(_))
|| matches!(self.height, Length::Ch(_))
|| matches!(self.min_height, Length::Ch(_))
|| matches!(self.max_height, Length::Ch(_))
|| matches!(self.flex_basis, Length::Ch(_))
}
pub(crate) fn resolve_ch(&mut self, ch_px: f32) {
self.width = self.width.resolved(ch_px);
self.min_width = self.min_width.resolved(ch_px);
self.max_width = self.max_width.resolved(ch_px);
self.height = self.height.resolved(ch_px);
self.min_height = self.min_height.resolved(ch_px);
self.max_height = self.max_height.resolved(ch_px);
self.flex_basis = self.flex_basis.resolved(ch_px);
}
}
#[cfg(test)]
mod feature_tests {
use super::*;
fn fs(style: Style) -> Option<String> {
style.text.features.feature_string()
}
#[test]
fn default_features_emit_nothing() {
assert_eq!(FontFeatures::default().feature_string(), None);
}
#[test]
fn tabular_unchanged() {
assert_eq!(
fs(Style::default().tabular()),
Some("\"tnum\" 1".to_owned())
);
}
#[test]
fn oldstyle_and_smcp() {
let s = fs(Style::default().oldstyle_nums().small_caps()).unwrap();
assert!(s.contains("\"onum\" 1"), "{s}");
assert!(s.contains("\"smcp\" 1"), "{s}");
assert!(!s.contains("\"lnum\""), "{s}");
assert!(!s.contains("\"tnum\""), "{s}");
assert!(!s.contains("\"pnum\""), "{s}");
}
#[test]
fn tnum_onum_mutually_consistent() {
let s = fs(Style::default().tabular().oldstyle_nums()).unwrap();
assert!(s.contains("\"tnum\" 1"), "{s}");
assert!(s.contains("\"onum\" 1"), "{s}");
}
#[test]
fn ligatures_off_and_on() {
assert!(
fs(Style::default().ligatures(false))
.unwrap()
.contains("\"liga\" 0")
);
assert!(
fs(Style::default().ligatures(true))
.unwrap()
.contains("\"liga\" 1")
);
}
#[test]
fn fractions_and_proportional() {
assert!(
fs(Style::default().fractions())
.unwrap()
.contains("\"frac\" 1")
);
assert!(
fs(Style::default().proportional_nums())
.unwrap()
.contains("\"pnum\" 1")
);
}
#[test]
fn figure_axis_is_exclusive() {
let style = Style::default().oldstyle_nums().lining_nums();
assert_eq!(style.text.features.figures, FigureStyle::Lining);
let s = fs(style).unwrap();
assert!(s.contains("\"lnum\""), "{s}");
assert!(!s.contains("\"onum\""), "{s}");
}
}