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,
Sticky,
}
#[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),
Auto,
MinContent,
MaxContent,
FitContent(f32),
MinMax(TrackMin, TrackMax),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TrackMin {
Px(f32),
Auto,
MinContent,
MaxContent,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TrackMax {
Px(f32),
Fr(f32),
Auto,
MinContent,
MaxContent,
FitContent(f32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum GridTemplate {
Single(Track),
Repeat(Repeat, Vec<Track>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Repeat {
Count(u16),
AutoFit,
AutoFill,
}
impl GridTemplate {
#[must_use]
pub fn auto_fit_minmax(min_px: f32) -> Self {
Self::Repeat(
Repeat::AutoFit,
vec![Track::MinMax(TrackMin::Px(min_px), TrackMax::Fr(1.0))],
)
}
#[must_use]
pub fn auto_fill_minmax(min_px: f32) -> Self {
Self::Repeat(
Repeat::AutoFill,
vec![Track::MinMax(TrackMin::Px(min_px), TrackMax::Fr(1.0))],
)
}
#[must_use]
pub fn repeat(n: u16, tracks: impl IntoIterator<Item = Track>) -> Self {
Self::Repeat(Repeat::Count(n), tracks.into_iter().collect())
}
}
impl From<Track> for GridTemplate {
fn from(t: Track) -> Self {
Self::Single(t)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct GridPlace {
pub start: Option<i16>,
pub span: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GridLines {
pub start: Option<String>,
pub end: Option<String>,
}
#[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>,
},
ConicGradient {
center: (f32, f32),
stops: Vec<GradientStop>,
},
}
impl From<Color> for Paint {
fn from(c: Color) -> Self {
Self::Solid(c)
}
}
#[must_use]
pub fn oklch_stops(anchors: &[(f32, Color)], steps: usize) -> Vec<GradientStop> {
if anchors.is_empty() {
return Vec::new();
}
let mut sorted = anchors.to_vec();
sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
if sorted.len() == 1 || steps == 0 {
return sorted
.iter()
.map(|&(offset, color)| GradientStop { offset, color })
.collect();
}
let mut out = Vec::with_capacity((sorted.len() - 1) * steps + 1);
for (seg, pair) in sorted.windows(2).enumerate() {
let (o0, c0) = pair[0];
let (o1, c1) = pair[1];
let first = usize::from(seg != 0);
for j in first..=steps {
#[expect(clippy::cast_precision_loss, reason = "gradient step counts are tiny")]
let t = j as f32 / steps as f32;
out.push(GradientStop {
offset: o0 + (o1 - o0) * t,
color: crate::anim::lerp_color(c0, c1, t),
});
}
}
out
}
fn even_anchors(colors: impl IntoIterator<Item = Color>) -> Vec<(f32, Color)> {
let colors: Vec<Color> = colors.into_iter().collect();
let last = colors.len().saturating_sub(1);
if last == 0 {
return colors.into_iter().map(|c| (0.0, c)).collect();
}
colors
.into_iter()
.enumerate()
.map(|(i, c)| {
#[expect(clippy::cast_precision_loss, reason = "color counts are tiny")]
let offset = i as f32 / last as f32;
(offset, c)
})
.collect()
}
fn degenerate_solid(colors: &[Color]) -> Option<Paint> {
match colors {
[] => Some(Paint::Solid(Color::new([0.0, 0.0, 0.0, 0.0]))),
[c] => Some(Paint::Solid(*c)),
_ => None,
}
}
#[must_use]
pub fn linear_gradient(angle_deg: f32, colors: impl IntoIterator<Item = Color>) -> Paint {
let colors: Vec<Color> = colors.into_iter().collect();
degenerate_solid(&colors).unwrap_or_else(|| Paint::LinearGradient {
angle_deg,
stops: oklch_stops(&even_anchors(colors), crate::tokens::GRADIENT_STEPS),
})
}
#[must_use]
pub fn radial_gradient(
center: (f32, f32),
radius: f32,
colors: impl IntoIterator<Item = Color>,
) -> Paint {
let colors: Vec<Color> = colors.into_iter().collect();
degenerate_solid(&colors).unwrap_or_else(|| Paint::RadialGradient {
center,
radius,
stops: oklch_stops(&even_anchors(colors), crate::tokens::GRADIENT_STEPS),
})
}
#[must_use]
pub fn conic_gradient(center: (f32, f32), colors: impl IntoIterator<Item = Color>) -> Paint {
let colors: Vec<Color> = colors.into_iter().collect();
degenerate_solid(&colors).unwrap_or_else(|| Paint::ConicGradient {
center,
stops: oklch_stops(&even_anchors(colors), crate::tokens::GRADIENT_STEPS),
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Border {
pub width: f32,
pub color: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct EdgeBorders {
pub top: Option<Border>,
pub right: Option<Border>,
pub bottom: Option<Border>,
pub left: Option<Border>,
}
#[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 TextWrap {
#[default]
Normal,
Balance,
Pretty,
}
#[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 enum OpticalSizing {
#[default]
Inherit,
Default,
Auto,
Fixed(f32),
}
impl OpticalSizing {
#[must_use]
pub fn opsz_at(self, px: f32) -> Option<f32> {
match self {
OpticalSizing::Inherit | OpticalSizing::Auto => Some(px.max(0.0)),
OpticalSizing::Default => None,
OpticalSizing::Fixed(v) => Some(v.max(0.0)),
}
}
}
#[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,
pub wrap: TextWrap,
pub optical: OpticalSizing,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ElementFilter {
Blur(f32),
Brightness(f32),
Saturate(f32),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpecularEdge {
pub light_deg: f32,
pub intensity: f32,
pub shade: f32,
}
impl SpecularEdge {
#[must_use]
pub const fn glass() -> Self {
Self {
light_deg: crate::tokens::GLASS_LIGHT_DEG,
intensity: 0.6,
shade: 0.18,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sheen {
pub light_deg: f32,
pub top: f32,
pub bottom: f32,
}
impl Sheen {
#[must_use]
pub const fn glass() -> Self {
Self {
light_deg: 135.0,
top: 0.12,
bottom: 0.06,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AdaptiveTint {
pub pivot: f32,
pub gain: f32,
}
impl AdaptiveTint {
#[must_use]
pub const fn glass() -> Self {
Self {
pivot: 0.55,
gain: 0.20,
}
}
}
#[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<GridTemplate>,
pub grid_template_rows: Vec<GridTemplate>,
pub grid_column: GridPlace,
pub grid_row: GridPlace,
pub grid_template_areas: Vec<Vec<Option<String>>>,
pub grid_area: Option<String>,
pub grid_column_lines: GridLines,
pub grid_row_lines: GridLines,
pub grid_col_line_names: Vec<Vec<String>>,
pub grid_row_line_names: Vec<Vec<String>>,
pub overflow_x: Overflow,
pub overflow_y: Overflow,
pub sticky_top: Option<f32>,
pub sticky_right: Option<f32>,
pub sticky_bottom: Option<f32>,
pub sticky_left: Option<f32>,
pub fill: Option<Paint>,
pub border: Option<Border>,
pub side_borders: EdgeBorders,
pub corner_radius: CornerRadius,
pub corner_smoothing: Option<f32>,
pub shadow_token: Option<crate::tokens::ShadowToken>,
pub shadows: Vec<Shadow>,
pub highlight_top: Option<Color>,
pub specular_edge: Option<SpecularEdge>,
pub sheen: Option<Sheen>,
pub adaptive_tint: Option<AdaptiveTint>,
pub opacity: f32,
pub scale: f32,
pub translate: (f32, f32),
pub rotate: f32,
pub skew: (f32, f32),
pub scale_xy: (f32, f32),
pub transform_origin: (f32, f32),
pub clip: bool,
pub path_trim: f32,
pub backdrop_blur: Option<f32>,
pub element_filter: Option<ElementFilter>,
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(),
grid_template_areas: Vec::new(),
grid_area: None,
grid_column_lines: GridLines::default(),
grid_row_lines: GridLines::default(),
grid_col_line_names: Vec::new(),
grid_row_line_names: Vec::new(),
overflow_x: Overflow::Visible,
overflow_y: Overflow::Visible,
sticky_top: None,
sticky_right: None,
sticky_bottom: None,
sticky_left: None,
fill: None,
border: None,
side_borders: EdgeBorders::default(),
corner_radius: CornerRadius::default(),
corner_smoothing: None,
shadow_token: None,
shadows: Vec::new(),
highlight_top: None,
specular_edge: None,
sheen: None,
adaptive_tint: None,
opacity: 1.0,
scale: 1.0,
translate: (0.0, 0.0),
rotate: 0.0,
skew: (0.0, 0.0),
scale_xy: (1.0, 1.0),
transform_origin: (0.5, 0.5),
clip: false,
path_trim: 1.0,
backdrop_blur: None,
element_filter: None,
text: TextStyle::default(),
}
}
}
impl Style {
#[must_use]
pub fn transform_origin_point(&self, rect: kurbo::Rect) -> kurbo::Point {
kurbo::Point::new(
rect.x0 + f64::from(self.transform_origin.0) * rect.width(),
rect.y0 + f64::from(self.transform_origin.1) * rect.height(),
)
}
pub fn paint_affine(&self, rect: kurbo::Rect) -> Option<kurbo::Affine> {
let s = self;
let has_transform = (s.scale - 1.0).abs() > 1e-4
|| (s.scale_xy.0 - 1.0).abs() > 1e-4
|| (s.scale_xy.1 - 1.0).abs() > 1e-4
|| s.translate.0.abs() > 1e-4
|| s.translate.1.abs() > 1e-4
|| s.rotate.abs() > 1e-4
|| s.skew.0.abs() > 1e-4
|| s.skew.1.abs() > 1e-4;
if !has_transform {
return None;
}
let p = s.transform_origin_point(rect);
let mut a = kurbo::Affine::translate((f64::from(s.translate.0), f64::from(s.translate.1)))
* kurbo::Affine::translate((p.x, p.y));
if s.rotate.abs() > 1e-4 {
a *= kurbo::Affine::rotate(f64::from(s.rotate).to_radians());
}
if s.skew.0.abs() > 1e-4 || s.skew.1.abs() > 1e-4 {
a *= kurbo::Affine::new([
1.0,
f64::from(s.skew.1).to_radians().tan(),
f64::from(s.skew.0).to_radians().tan(),
1.0,
0.0,
0.0,
]);
}
if (s.scale - 1.0).abs() > 1e-4 {
a *= kurbo::Affine::scale(f64::from(s.scale));
}
if (s.scale_xy.0 - 1.0).abs() > 1e-4 || (s.scale_xy.1 - 1.0).abs() > 1e-4 {
a *= kurbo::Affine::scale_non_uniform(f64::from(s.scale_xy.0), f64::from(s.scale_xy.1));
}
a *= kurbo::Affine::translate((-p.x, -p.y));
Some(a)
}
}
pub type ThemedFn = Box<dyn Fn(&crate::theme::Theme, Style) -> Style>;
pub use fenestra_anim::SpringSpec;
#[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 self_start(mut self) -> Self {
self.align_self = Some(AlignItems::Start);
self
}
pub fn self_center(mut self) -> Self {
self.align_self = Some(AlignItems::Center);
self
}
pub fn self_end(mut self) -> Self {
self.align_self = Some(AlignItems::End);
self
}
pub fn self_stretch(mut self) -> Self {
self.align_self = Some(AlignItems::Stretch);
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 scroll_x(mut self) -> Self {
self.overflow_x = Overflow::Scroll;
self.clip = true;
self
}
pub fn scroll_xy(mut self) -> Self {
self.overflow_x = Overflow::Scroll;
self.overflow_y = Overflow::Scroll;
self.clip = true;
self
}
pub fn sticky_top(mut self, offset: f32) -> Self {
self.position = Position::Sticky;
self.sticky_top = Some(offset);
self
}
pub fn sticky_bottom(mut self, offset: f32) -> Self {
self.position = Position::Sticky;
self.sticky_bottom = Some(offset);
self
}
pub fn sticky_left(mut self, offset: f32) -> Self {
self.position = Position::Sticky;
self.sticky_left = Some(offset);
self
}
pub fn sticky_right(mut self, offset: f32) -> Self {
self.position = Position::Sticky;
self.sticky_right = Some(offset);
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 border_top(mut self, width: f32, color: Color) -> Self {
self.side_borders.top = Some(Border { width, color });
self
}
pub fn border_right(mut self, width: f32, color: Color) -> Self {
self.side_borders.right = Some(Border { width, color });
self
}
pub fn border_bottom(mut self, width: f32, color: Color) -> Self {
self.side_borders.bottom = Some(Border { width, color });
self
}
pub fn border_left(mut self, width: f32, color: Color) -> Self {
self.side_borders.left = Some(Border { width, color });
self
}
pub fn ring(mut self, width: f32, color: Color) -> Self {
self.shadows.push(Shadow {
dx: 0.0,
dy: 0.0,
blur: 0.0,
spread: 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 rounded_t(mut self, r: f32) -> Self {
self.corner_radius.tl = r;
self.corner_radius.tr = r;
self
}
pub fn rounded_b(mut self, r: f32) -> Self {
self.corner_radius.br = r;
self.corner_radius.bl = r;
self
}
pub fn rounded_l(mut self, r: f32) -> Self {
self.corner_radius.tl = r;
self.corner_radius.bl = r;
self
}
pub fn rounded_r(mut self, r: f32) -> Self {
self.corner_radius.tr = r;
self.corner_radius.br = r;
self
}
pub fn corners(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
self.corner_radius = CornerRadius { tl, tr, br, bl };
self
}
pub fn corner_smoothing(mut self, s: f32) -> Self {
self.corner_smoothing = Some(s.clamp(0.0, 1.0));
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 specular_edge(mut self, edge: SpecularEdge) -> Self {
self.specular_edge = Some(edge);
self
}
pub fn sheen(mut self, sheen: Sheen) -> Self {
self.sheen = Some(sheen);
self
}
pub fn adaptive_tint(mut self, adaptive: AdaptiveTint) -> Self {
self.adaptive_tint = Some(adaptive);
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 translate(mut self, x: f32, y: f32) -> Self {
self.translate = (x, y);
self
}
pub fn rotate(mut self, degrees: f32) -> Self {
self.rotate = degrees;
self
}
pub fn skew(mut self, x_degrees: f32, y_degrees: f32) -> Self {
self.skew = (x_degrees, y_degrees);
self
}
pub fn scale_xy(mut self, x: f32, y: f32) -> Self {
self.scale_xy = (x, y);
self
}
pub fn transform_origin(mut self, fx: f32, fy: f32) -> Self {
self.transform_origin = (fx, fy);
self
}
pub fn trim(mut self, v: f32) -> Self {
self.path_trim = v.clamp(0.0, 1.0);
self
}
pub fn backdrop_blur(mut self, radius: f32) -> Self {
self.backdrop_blur = (radius > 0.0).then_some(radius);
self
}
pub fn element_filter(mut self, filter: ElementFilter) -> Self {
self.element_filter = Some(filter);
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
}
pub fn balance(mut self) -> Self {
self.text.wrap = TextWrap::Balance;
self
}
pub fn pretty(mut self) -> Self {
self.text.wrap = TextWrap::Pretty;
self
}
pub fn text_wrap(mut self, wrap: TextWrap) -> Self {
self.text.wrap = wrap;
self
}
pub fn optical(mut self, optical: OpticalSizing) -> Self {
self.text.optical = optical;
self
}
pub fn optical_auto(mut self) -> Self {
self.text.optical = OpticalSizing::Auto;
self
}
}
impl Style {
pub fn grid_cols<T: Into<GridTemplate>>(mut self, tracks: impl IntoIterator<Item = T>) -> Self {
self.display = Display::Grid;
self.grid_template_columns = tracks.into_iter().map(Into::into).collect();
self
}
pub fn grid_rows<T: Into<GridTemplate>>(mut self, tracks: impl IntoIterator<Item = T>) -> Self {
self.display = Display::Grid;
self.grid_template_rows = tracks.into_iter().map(Into::into).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
}
pub fn grid_template_areas<R: AsRef<str>>(mut self, rows: impl IntoIterator<Item = R>) -> Self {
self.display = Display::Grid;
self.grid_template_areas = rows
.into_iter()
.map(|row| {
row.as_ref()
.split_whitespace()
.map(|cell| (cell != ".").then(|| cell.to_string()))
.collect()
})
.collect();
self
}
pub fn grid_area(mut self, name: impl Into<String>) -> Self {
self.grid_area = Some(name.into());
self
}
pub fn grid_col_lines(mut self, start: impl Into<String>, end: impl Into<String>) -> Self {
self.grid_column_lines = GridLines {
start: Some(start.into()),
end: Some(end.into()),
};
self
}
pub fn grid_row_lines(mut self, start: impl Into<String>, end: impl Into<String>) -> Self {
self.grid_row_lines = GridLines {
start: Some(start.into()),
end: Some(end.into()),
};
self
}
pub fn grid_col_names<S: Into<String>>(mut self, names: impl IntoIterator<Item = S>) -> Self {
self.grid_col_line_names = names.into_iter().map(|n| vec![n.into()]).collect();
self
}
pub fn grid_row_names<S: Into<String>>(mut self, names: impl IntoIterator<Item = S>) -> Self {
self.grid_row_line_names = names.into_iter().map(|n| vec![n.into()]).collect();
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 corner_smoothing_tests {
use super::*;
#[test]
fn corner_smoothing_defaults_none_and_clamps() {
assert_eq!(Style::default().corner_smoothing, None);
assert_eq!(
Style::default().corner_smoothing(0.6).corner_smoothing,
Some(0.6)
);
assert_eq!(
Style::default().corner_smoothing(5.0).corner_smoothing,
Some(1.0)
);
assert_eq!(
Style::default().corner_smoothing(-1.0).corner_smoothing,
Some(0.0)
);
}
}
#[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}");
}
}
#[cfg(test)]
mod gradient_tests {
use super::*;
use crate::oklch_of;
use crate::theme::Theme;
use crate::tokens::GRADIENT_STEPS;
#[test]
fn midpoint_keeps_chroma_no_gray_deadzone() {
let theme = Theme::light();
let a = theme.accent;
let b = theme.warning.solid;
let stops = oklch_stops(&[(0.0, a), (1.0, b)], GRADIENT_STEPS);
let mid = stops
.iter()
.min_by(|x, y| (x.offset - 0.5).abs().total_cmp(&(y.offset - 0.5).abs()))
.unwrap();
let ca = a.components;
let cb = b.components;
let srgb_mid = Color::new([
(ca[0] + cb[0]) / 2.0,
(ca[1] + cb[1]) / 2.0,
(ca[2] + cb[2]) / 2.0,
(ca[3] + cb[3]) / 2.0,
]);
let c_oklch = oklch_of(mid.color)[1];
let c_srgb = oklch_of(srgb_mid)[1];
assert!(
c_oklch > 1.5 * c_srgb,
"OKLCH mid chroma {c_oklch} should far exceed sRGB mid chroma {c_srgb}"
);
}
#[test]
fn lightness_is_monotonic_across_stops() {
let theme = Theme::light();
let a = theme.accents.step(7);
let b = theme.accents.step(10);
let stops = oklch_stops(&[(0.0, a), (1.0, b)], GRADIENT_STEPS);
for w in stops.windows(2) {
let l0 = oklch_of(w[0].color)[0];
let l1 = oklch_of(w[1].color)[0];
assert!(l1 <= l0 + 1e-3, "lightness rose mid-ramp: {l0} -> {l1}");
}
}
#[test]
fn offsets_sorted_and_span_anchors() {
let theme = Theme::light();
let a = theme.accent;
let b = theme.warning.solid;
let stops = oklch_stops(&[(0.0, a), (1.0, b)], 16);
assert_eq!(stops.len(), 17);
assert_eq!(stops.first().unwrap().offset, 0.0);
assert_eq!(stops.last().unwrap().offset, 1.0);
for w in stops.windows(2) {
assert!(w[1].offset > w[0].offset, "offsets must strictly increase");
}
let unsorted = oklch_stops(&[(1.0, b), (0.0, a)], 16);
assert_eq!(unsorted, stops);
}
#[test]
fn endpoints_are_exact() {
let theme = Theme::light();
let a = theme.accent;
let b = theme.warning.solid;
let stops = oklch_stops(&[(0.0, a), (1.0, b)], 16);
assert_eq!(stops.first().unwrap().color.to_rgba8(), a.to_rgba8());
assert_eq!(stops.last().unwrap().color.to_rgba8(), b.to_rgba8());
}
#[test]
fn linear_gradient_even_spacing() {
let theme = Theme::light();
let (a, b, c) = (theme.accent, theme.warning.solid, theme.success.solid);
let paint = linear_gradient(90.0, [a, b, c]);
let Paint::LinearGradient { angle_deg, stops } = paint else {
panic!("linear_gradient must build a LinearGradient");
};
assert_eq!(angle_deg, 90.0);
for (off, color) in [(0.0, a), (0.5, b), (1.0, c)] {
let found = stops
.iter()
.find(|s| (s.offset - off).abs() < 1e-4)
.unwrap_or_else(|| panic!("no stop at offset {off}"));
assert_eq!(
found.color.to_rgba8(),
color.to_rgba8(),
"anchor color at offset {off}"
);
}
}
#[test]
fn degenerate_inputs() {
let theme = Theme::light();
let a = theme.accent;
let b = theme.warning.solid;
assert!(oklch_stops(&[], 16).is_empty());
let single = oklch_stops(&[(0.3, a)], 16);
assert_eq!(single.len(), 1);
assert_eq!(single[0].offset, 0.3);
assert_eq!(single[0].color.to_rgba8(), a.to_rgba8());
let zero_steps = oklch_stops(&[(0.0, a), (1.0, b)], 0);
assert_eq!(zero_steps.len(), 2);
assert_eq!(zero_steps[0].color.to_rgba8(), a.to_rgba8());
assert_eq!(zero_steps[1].color.to_rgba8(), b.to_rgba8());
}
#[test]
fn optical_sizing_resolves_opsz_per_mode() {
assert_eq!(OpticalSizing::Default.opsz_at(16.0), None);
assert_eq!(OpticalSizing::Default.opsz_at(48.0), None);
assert_eq!(OpticalSizing::Auto.opsz_at(16.0), Some(16.0));
assert_eq!(OpticalSizing::Auto.opsz_at(48.0), Some(48.0));
assert_eq!(OpticalSizing::Fixed(72.0).opsz_at(16.0), Some(72.0));
assert_eq!(OpticalSizing::Fixed(72.0).opsz_at(48.0), Some(72.0));
assert_eq!(OpticalSizing::Fixed(-5.0).opsz_at(16.0), Some(0.0));
assert_eq!(OpticalSizing::Inherit.opsz_at(16.0), Some(16.0));
assert_eq!(OpticalSizing::default(), OpticalSizing::Inherit);
}
#[test]
fn optical_builders_set_the_axis() {
assert_eq!(Style::default().text.optical, OpticalSizing::Inherit);
assert_eq!(
Style::default().optical_auto().text.optical,
OpticalSizing::Auto
);
assert_eq!(
Style::default()
.optical(OpticalSizing::Fixed(60.0))
.text
.optical,
OpticalSizing::Fixed(60.0)
);
}
}