use crate::tokens;
use crate::tree::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum StyleProfile {
Solid,
Tinted,
Surface,
#[default]
TextOnly,
}
impl El {
pub fn primary(self) -> Self {
tint(self, tokens::PRIMARY)
}
pub fn success(self) -> Self {
tint(self, tokens::SUCCESS)
}
pub fn warning(self) -> Self {
tint(self, tokens::WARNING)
}
pub fn destructive(self) -> Self {
tint(self, tokens::DESTRUCTIVE)
}
pub fn info(self) -> Self {
tint(self, tokens::INFO)
}
pub fn secondary(mut self) -> Self {
self.fill = Some(tokens::BG_MUTED);
self.stroke = Some(tokens::BORDER);
self.stroke_width = 1.0;
set_content_color(&mut self, tokens::TEXT_FOREGROUND);
self.font_weight = FontWeight::Medium;
self
}
pub fn ghost(mut self) -> Self {
self.fill = None;
self.stroke = None;
self.stroke_width = 0.0;
set_content_color(&mut self, tokens::TEXT_MUTED_FOREGROUND);
self
}
pub fn outline(mut self) -> Self {
self.fill = None;
self.stroke = Some(tokens::BORDER_STRONG);
self.stroke_width = 1.0;
set_content_color(&mut self, tokens::TEXT_FOREGROUND);
self
}
pub fn muted(mut self) -> Self {
match self.style_profile {
StyleProfile::Solid | StyleProfile::Tinted | StyleProfile::Surface => {
self.fill = Some(tokens::BG_MUTED);
self.stroke = Some(tokens::BORDER);
self.stroke_width = 1.0;
set_content_color(&mut self, tokens::TEXT_MUTED_FOREGROUND);
}
StyleProfile::TextOnly => {
set_content_color(&mut self, tokens::TEXT_MUTED_FOREGROUND);
}
}
self
}
pub fn selected(mut self) -> Self {
if text_only_leaf(&self) {
self.text_color = Some(tokens::PRIMARY);
} else {
match self.style_profile {
StyleProfile::TextOnly => {}
StyleProfile::Solid | StyleProfile::Tinted | StyleProfile::Surface => {}
}
{
self.style_profile = StyleProfile::Surface;
self.surface_role = SurfaceRole::Selected;
self.fill = Some(tokens::PRIMARY.with_alpha(28));
self.stroke = Some(tokens::PRIMARY.with_alpha(90));
self.stroke_width = 1.0;
set_content_color(&mut self, tokens::TEXT_FOREGROUND);
}
}
self
}
pub fn current(mut self) -> Self {
if text_only_leaf(&self) {
self.text_color = Some(tokens::TEXT_FOREGROUND);
self.font_weight = FontWeight::Semibold;
} else {
self.style_profile = StyleProfile::Surface;
self.surface_role = SurfaceRole::Current;
self.fill = Some(tokens::BG_RAISED);
self.stroke = Some(tokens::BORDER);
self.stroke_width = 1.0;
set_content_color(&mut self, tokens::TEXT_FOREGROUND);
self.font_weight = FontWeight::Semibold;
}
self
}
pub fn disabled(mut self) -> Self {
self.opacity = tokens::DISABLED_ALPHA;
self.focusable = false;
self.block_pointer = true;
if text_only_leaf(&self) {
self.text_color = Some(tokens::TEXT_MUTED_FOREGROUND);
}
self
}
pub fn invalid(mut self) -> Self {
if !text_only_leaf(&self) {
self.style_profile = StyleProfile::Surface;
self.surface_role = SurfaceRole::Danger;
}
self.stroke = Some(tokens::DESTRUCTIVE);
self.stroke_width = 1.0;
if text_only_leaf(&self) {
self.text_color = Some(tokens::DESTRUCTIVE);
}
self
}
pub fn loading(mut self) -> Self {
self.opacity = self.opacity.min(0.78);
if let Some(label) = &mut self.text {
label.push_str("...");
}
self
}
pub fn text_role(mut self, role: TextRole) -> Self {
self.text_role = role;
apply_text_role(&mut self);
self
}
pub fn caption(self) -> Self {
self.text_role(TextRole::Caption)
}
pub fn label(self) -> Self {
self.text_role(TextRole::Label)
}
pub fn body(self) -> Self {
self.text_role(TextRole::Body)
}
pub fn title(self) -> Self {
self.text_role(TextRole::Title)
}
pub fn heading(self) -> Self {
self.text_role(TextRole::Heading)
}
pub fn display(self) -> Self {
self.text_role(TextRole::Display)
}
pub fn bold(mut self) -> Self {
self.font_weight = FontWeight::Bold;
self
}
pub fn semibold(mut self) -> Self {
self.font_weight = FontWeight::Semibold;
self
}
pub fn small(mut self) -> Self {
self.font_size = tokens::FONT_SM;
self
}
pub fn xsmall(mut self) -> Self {
self.font_size = tokens::FONT_XS;
self
}
pub fn color(mut self, c: Color) -> Self {
self.text_color = Some(c);
self
}
}
fn text_only_leaf(el: &El) -> bool {
matches!(el.style_profile, StyleProfile::TextOnly) && el.text.is_some()
}
fn apply_text_role(el: &mut El) {
match el.text_role {
TextRole::Body => {
el.font_size = tokens::FONT_BASE;
el.font_weight = FontWeight::Regular;
el.font_mono = false;
el.text_color = Some(tokens::TEXT_FOREGROUND);
}
TextRole::Caption => {
el.font_size = tokens::FONT_XS;
el.font_weight = FontWeight::Regular;
el.font_mono = false;
el.text_color = Some(tokens::TEXT_MUTED_FOREGROUND);
}
TextRole::Label => {
el.font_size = tokens::FONT_BASE;
el.font_weight = FontWeight::Medium;
el.font_mono = false;
el.text_color = Some(tokens::TEXT_FOREGROUND);
}
TextRole::Title => {
el.font_size = tokens::FONT_LG;
el.font_weight = FontWeight::Semibold;
el.font_mono = false;
el.text_color = Some(tokens::TEXT_FOREGROUND);
}
TextRole::Heading => {
el.font_size = tokens::FONT_XL;
el.font_weight = FontWeight::Semibold;
el.font_mono = false;
el.text_color = Some(tokens::TEXT_FOREGROUND);
}
TextRole::Display => {
el.font_size = tokens::FONT_XXL;
el.font_weight = FontWeight::Bold;
el.font_mono = false;
el.text_color = Some(tokens::TEXT_FOREGROUND);
}
TextRole::Code => {
el.font_size = tokens::FONT_SM;
el.font_weight = FontWeight::Regular;
el.font_mono = true;
el.text_color = Some(tokens::TEXT_FOREGROUND);
}
}
}
fn tint(mut el: El, c: Color) -> El {
match el.style_profile {
StyleProfile::Solid => {
el.fill = Some(c);
el.stroke = Some(c);
el.stroke_width = 1.0;
set_content_color(&mut el, text_on_solid(c));
el.font_weight = FontWeight::Semibold;
}
StyleProfile::Tinted => {
el.fill = Some(c.with_alpha(38));
el.stroke = Some(c.with_alpha(120));
el.stroke_width = 1.0;
set_content_color(&mut el, c);
}
StyleProfile::Surface => {
el.fill = Some(c.with_alpha(38));
el.stroke = Some(c.with_alpha(120));
el.stroke_width = 1.0;
set_content_color(&mut el, c);
}
StyleProfile::TextOnly => {
set_content_color(&mut el, c);
}
}
el
}
fn set_content_color(el: &mut El, color: Color) {
el.text_color = Some(color);
for child in &mut el.children {
if child.text.is_some() || child.icon.is_some() {
child.text_color = Some(color);
}
}
}
fn text_on_solid(c: Color) -> Color {
let lum = 0.299 * c.r as f32 + 0.587 * c.g as f32 + 0.114 * c.b as f32;
if lum > 150.0 {
tokens::TEXT_ON_SOLID_DARK
} else {
tokens::TEXT_ON_SOLID_LIGHT
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{button, button_with_icon, icon_button, row, text};
#[test]
fn selected_marks_surface_with_accent_treatment() {
let el = row([text("Selected")]).selected();
assert_eq!(el.fill, Some(tokens::PRIMARY.with_alpha(28)));
assert_eq!(el.stroke, Some(tokens::PRIMARY.with_alpha(90)));
assert_eq!(el.stroke_width, 1.0);
assert_eq!(el.surface_role, SurfaceRole::Selected);
}
#[test]
fn current_marks_container_as_selected_surface_role() {
let el = row([text("Current")]).current();
assert_eq!(el.fill, Some(tokens::BG_RAISED));
assert_eq!(el.stroke, Some(tokens::BORDER));
assert_eq!(el.surface_role, SurfaceRole::Current);
assert_eq!(el.style_profile, StyleProfile::Surface);
}
#[test]
fn disabled_removes_focus_and_dims_control() {
let el = button("Disabled").disabled();
assert!(!el.focusable);
assert!(el.block_pointer);
assert_eq!(el.opacity, tokens::DISABLED_ALPHA);
}
#[test]
fn icon_button_uses_same_solid_style_surface_as_button() {
let el = icon_button("menu").primary();
assert_eq!(el.icon, Some(IconName::Menu));
assert_eq!(el.fill, Some(tokens::PRIMARY));
assert_eq!(el.text_color, Some(tokens::TEXT_ON_SOLID_DARK));
assert_eq!(el.surface_role, SurfaceRole::Raised);
}
#[test]
fn button_with_icon_propagates_variant_content_color() {
let el = button_with_icon("upload", "Publish").primary();
assert_eq!(el.fill, Some(tokens::PRIMARY));
assert_eq!(el.children[0].icon, Some(IconName::Upload));
assert_eq!(el.children[0].text_color, Some(tokens::TEXT_ON_SOLID_DARK));
assert_eq!(el.children[1].text.as_deref(), Some("Publish"));
assert_eq!(el.children[1].text_color, Some(tokens::TEXT_ON_SOLID_DARK));
}
#[test]
fn loading_appends_direct_label_text() {
let el = button("Save").loading();
assert_eq!(el.text.as_deref(), Some("Save..."));
assert_eq!(el.opacity, 0.78);
}
#[test]
fn text_roles_apply_inspectable_typographic_defaults() {
let caption = text("Caption").caption();
assert_eq!(caption.text_role, TextRole::Caption);
assert_eq!(caption.font_size, tokens::FONT_XS);
assert_eq!(caption.text_color, Some(tokens::TEXT_MUTED_FOREGROUND));
let label = text("Label").label();
assert_eq!(label.text_role, TextRole::Label);
assert_eq!(label.font_size, tokens::FONT_BASE);
assert_eq!(label.font_weight, FontWeight::Medium);
let code = text("Code").code();
assert_eq!(code.text_role, TextRole::Code);
assert!(code.font_mono);
}
}