♻️ [bootsier] Refactoriza la gestión de clases

- Mejora la legibilidad del código.
- Simplifica las alteraciones de clases en los componentes `Container`,
  `Dropdown`, `Image`, `Nav`, `Navbar` y `Offcanvas` usando métodos
  dedicados para generar clases en función de sus propiedades.
- Mejora los enums añadiendo métodos que devuelven sus clases
  asociadas, reduciendo código repetitivo.
- Elimina el trait `JoinClasses` y su implementación, integrando la
  lógica de unión de clases directamente en los componentes.
This commit is contained in:
Manuel Cillero 2025-11-15 13:16:15 +01:00
parent 39033ef641
commit 623ef7e2c7
33 changed files with 1607 additions and 647 deletions

View file

@ -1,122 +1,113 @@
use pagetop::prelude::*;
use crate::theme::aux::{BorderColor, BorderSize, Opacity};
use std::fmt;
use crate::theme::aux::{BorderColor, Opacity, ScaleSize, Side};
/// Clases para crear **bordes**.
///
/// Permite:
///
/// - Iniciar un borde sin tamaño inicial (`Border::default()`).
/// - Crear un borde con tamaño por defecto (`Border::new()`).
/// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL).
/// - Definir un tamaño **global** para todo el borde (`size`).
/// - Ajustar el tamaño de cada **lado lógico** (`top`, `end`, `bottom`, `start`, **en este orden**,
/// respetando LTR/RTL).
/// - Aplicar un **color** al borde (`BorderColor`).
/// - Aplicar un nivel de **opacidad** (`Opacity`).
///
/// # Comportamiento aditivo / sustractivo
///
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::new()` para ir
/// añadiendo cada lado lógico con el tamaño deseado usando `BorderSize::Scale{1..5}`.
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::default()` para ir
/// añadiendo cada lado lógico con el tamaño deseado usando `ScaleSize::{One..Five}`.
///
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. utilizando
/// `classes::Border::with(BorderSize::Scale2)` y eliminar los lados deseados con `BorderSize::Zero`.
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. usando
/// `classes::Border::new()` o `classes::Border::with(ScaleSize::Two)` y eliminar los lados
/// deseados con `ScaleSize::Zero`.
///
/// - **Anchos diferentes por lado**: usando `BorderSize::Scale{1..5}` en cada lado deseado.
/// - **Anchos diferentes por lado**: usando `ScaleSize::{Zero..Five}` en cada lado deseado.
///
/// # Ejemplos
///
/// **Borde global:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(BorderSize::Scale2);
/// assert_eq!(b.to_string(), "border-2");
/// let b = classes::Border::with(ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
/// ```
///
/// **Aditivo (solo borde superior):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::new().with_top(BorderSize::Scale1);
/// assert_eq!(b.to_string(), "border-top-1");
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
/// assert_eq!(b.to_class(), "border-top-1");
/// ```
///
/// **Sustractivo (borde global menos el superior):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(BorderSize::Default).with_top(BorderSize::Zero);
/// assert_eq!(b.to_string(), "border border-top-0");
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
/// assert_eq!(b.to_class(), "border border-top-0");
/// ```
///
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::new().with_start(BorderSize::Scale2).with_end(BorderSize::Scale4);
/// assert_eq!(b.to_string(), "border-end-4 border-start-2");
/// let b = classes::Border::default()
/// .with_side(Side::Start, ScaleSize::Two)
/// .with_side(Side::End, ScaleSize::Four);
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
/// ```
///
/// **Combinado (ejemplo completo):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(BorderSize::Default) // Borde global por defecto.
/// .with_top(BorderSize::Zero) // Quita borde superior.
/// .with_end(BorderSize::Scale3) // Ancho 3 para el lado lógico final.
/// let b = classes::Border::new() // Borde por defecto.
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
/// .with_color(BorderColor::Theme(Color::Primary))
/// .with_opacity(Opacity::Half);
///
/// assert_eq!(b.to_string(), "border border-top-0 border-end-3 border-primary border-opacity-50");
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Border {
size : BorderSize,
top : BorderSize,
end : BorderSize,
bottom : BorderSize,
start : BorderSize,
all : ScaleSize,
top : ScaleSize,
end : ScaleSize,
bottom : ScaleSize,
start : ScaleSize,
color : BorderColor,
opacity: Opacity,
}
impl Border {
/// Prepara un borde **sin tamaño global** de partida.
/// Prepara un borde del tamaño predefinido. Equivale a `border` (ancho por defecto del tema).
pub fn new() -> Self {
Self::default()
Self::with(ScaleSize::Auto)
}
/// Crea un borde **con tamaño global** (`size`).
pub fn with(size: BorderSize) -> Self {
Self::default().with_size(size)
/// Crea un borde **con un tamaño global** (`size`).
pub fn with(size: ScaleSize) -> Self {
Self::default().with_side(Side::All, size)
}
// **< Border BUILDER >*************************************************************************
/// Establece el tamaño global del borde (`border*`).
pub fn with_size(mut self, size: BorderSize) -> Self {
self.size = size;
self
}
/// Establece el tamaño del borde superior (`border-top-*`).
pub fn with_top(mut self, size: BorderSize) -> Self {
self.top = size;
self
}
/// Establece el tamaño del borde en el lado lógico final (`border-end-*`). Respeta LTR/RTL.
pub fn with_end(mut self, size: BorderSize) -> Self {
self.end = size;
self
}
/// Establece el tamaño del borde inferior (`border-bottom-*`).
pub fn with_bottom(mut self, size: BorderSize) -> Self {
self.bottom = size;
self
}
/// Establece el tamaño del borde en el lado lógico inicial (`border-start-*`). Respeta LTR/RTL.
pub fn with_start(mut self, size: BorderSize) -> Self {
self.start = size;
pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self {
match side {
Side::All => self.all = size,
Side::Top => self.top = size,
Side::Bottom => self.bottom = size,
Side::Start => self.start = size,
Side::End => self.end = size,
Side::LeftAndRight => {
self.start = size;
self.end = size;
}
Side::TopAndBottom => {
self.top = size;
self.bottom = size;
}
};
self
}
@ -131,25 +122,54 @@ impl Border {
self.opacity = opacity;
self
}
}
impl fmt::Display for Border {
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`, *color*
/// y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
[
self.size.to_string(),
self.top.to_class("border-top"),
self.end.to_class("border-end"),
self.bottom.to_class("border-bottom"),
self.start.to_class("border-start"),
self.color.to_string(),
self.opacity.to_class("border"),
]
.join_classes()
)
// **< Border HELPERS >*************************************************************************
/// Añade las clases de borde a la cadena de clases.
///
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// *color* y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
#[rustfmt::skip]
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.all .push_class(classes, "border");
self.top .push_class(classes, "border-top");
self.end .push_class(classes, "border-end");
self.bottom .push_class(classes, "border-bottom");
self.start .push_class(classes, "border-start");
self.color .push_class(classes);
self.opacity.push_class(classes, "border");
}
/// Devuelve las clases de borde como cadena (`"border-2"`,
/// `"border border-top-0 border-end-3 border-primary border-opacity-50"`, etc.).
///
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
/// Atajo para crear un [`classes::Border`](crate::theme::classes::Border) a partir de un tamaño
/// [`ScaleSize`] aplicado a todo el borde.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// // Convertir explícitamente con `From::from`:
/// let b = classes::Border::from(ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
///
/// // Convertir implícitamente con `into()`:
/// let b: classes::Border = ScaleSize::Auto.into();
/// assert_eq!(b.to_class(), "border");
/// ```
impl From<ScaleSize> for Border {
fn from(size: ScaleSize) -> Self {
Self::with(size)
}
}

View file

@ -2,9 +2,7 @@ use pagetop::prelude::*;
use crate::theme::aux::{ColorBg, ColorText, Opacity};
use std::fmt;
// **< Bg >*****************************************************************************************
// **< Background >*********************************************************************************
/// Clases para establecer **color/opacidad del fondo**.
///
@ -14,28 +12,27 @@ use std::fmt;
/// # use pagetop_bootsier::prelude::*;
/// // Sin clases.
/// let s = classes::Background::new();
/// assert_eq!(s.to_string(), "");
/// assert_eq!(s.to_class(), "");
///
/// // Sólo color de fondo.
/// let s = classes::Background::with(ColorBg::Theme(Color::Primary));
/// assert_eq!(s.to_string(), "bg-primary");
/// assert_eq!(s.to_class(), "bg-primary");
///
/// // Color más opacidad.
/// let s = classes::Background::with(ColorBg::BodySecondary).with_opacity(Opacity::Half);
/// assert_eq!(s.to_string(), "bg-body-secondary bg-opacity-50");
/// assert_eq!(s.to_class(), "bg-body-secondary bg-opacity-50");
///
/// // Usando `From<ColorBg>`.
/// let s: classes::Background = ColorBg::Black.into();
/// assert_eq!(s.to_string(), "bg-black");
/// assert_eq!(s.to_class(), "bg-black");
///
/// // Usando `From<(ColorBg, Opacity)>`.
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// assert_eq!(s.to_string(), "bg-white bg-opacity-25");
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Background {
color : ColorBg,
color: ColorBg,
opacity: Opacity,
}
@ -50,7 +47,7 @@ impl Background {
Self::default().with_color(color)
}
// **< Bg BUILDER >*****************************************************************************
// **< Background BUILDER >*********************************************************************
/// Establece el color de fondo (`bg-*`).
pub fn with_color(mut self, color: ColorBg) -> Self {
@ -63,14 +60,27 @@ impl Background {
self.opacity = opacity;
self
}
}
impl fmt::Display for Background {
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`), omitiendo
/// las definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let classes = [self.color.to_string(), self.opacity.to_class("bg")].join_classes();
write!(f, "{classes}")
// **< Background HELPERS >*********************************************************************
/// Añade las clases de fondo a la cadena de clases.
///
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`),
/// omitiendo los fragmentos vacíos.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.color.push_class(classes);
self.opacity.push_class(classes, "bg");
}
/// Devuelve las clases de fondo como cadena (`"bg-primary"`, `"bg-body-secondary bg-opacity-50"`, etc.).
///
/// Si no se define ni color ni opacidad, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
@ -83,7 +93,7 @@ impl From<(ColorBg, Opacity)> for Background {
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// assert_eq!(s.to_string(), "bg-white bg-opacity-25");
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
/// ```
fn from((color, opacity): (ColorBg, Opacity)) -> Self {
Background::with(color).with_opacity(opacity)
@ -98,7 +108,7 @@ impl From<ColorBg> for Background {
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Background = ColorBg::Black.into();
/// assert_eq!(s.to_string(), "bg-black");
/// assert_eq!(s.to_class(), "bg-black");
/// ```
fn from(color: ColorBg) -> Self {
Background::with(color)
@ -115,28 +125,27 @@ impl From<ColorBg> for Background {
/// # use pagetop_bootsier::prelude::*;
/// // Sin clases.
/// let s = classes::Text::new();
/// assert_eq!(s.to_string(), "");
/// assert_eq!(s.to_class(), "");
///
/// // Sólo color del texto.
/// let s = classes::Text::with(ColorText::Theme(Color::Primary));
/// assert_eq!(s.to_string(), "text-primary");
/// assert_eq!(s.to_class(), "text-primary");
///
/// // Color del texto y opacidad.
/// let s = classes::Text::new().with_color(ColorText::White).with_opacity(Opacity::SemiTransparent);
/// assert_eq!(s.to_string(), "text-white text-opacity-25");
/// assert_eq!(s.to_class(), "text-white text-opacity-25");
///
/// // Usando `From<ColorText>`.
/// let s: classes::Text = ColorText::Black.into();
/// assert_eq!(s.to_string(), "text-black");
/// assert_eq!(s.to_class(), "text-black");
///
/// // Usando `From<(ColorText, Opacity)>`.
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// assert_eq!(s.to_string(), "text-danger text-opacity-100");
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Text {
color : ColorText,
color: ColorText,
opacity: Opacity,
}
@ -164,13 +173,27 @@ impl Text {
self.opacity = opacity;
self
}
}
impl fmt::Display for Text {
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo las definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let classes = [self.color.to_string(), self.opacity.to_class("text")].join_classes();
write!(f, "{classes}")
// **< Text HELPERS >***************************************************************************
/// Añade las clases de texto a la cadena de clases.
///
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo los fragmentos vacíos.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.color.push_class(classes);
self.opacity.push_class(classes, "text");
}
/// Devuelve las clases de texto como cadena (`"text-primary"`, `"text-white text-opacity-25"`,
/// etc.).
///
/// Si no se define ni color ni opacidad, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
@ -183,7 +206,7 @@ impl From<(ColorText, Opacity)> for Text {
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// assert_eq!(s.to_string(), "text-danger text-opacity-100");
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
/// ```
fn from((color, opacity): (ColorText, Opacity)) -> Self {
Text::with(color).with_opacity(opacity)
@ -199,7 +222,7 @@ impl From<ColorText> for Text {
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Text = ColorText::Black.into();
/// assert_eq!(s.to_string(), "text-black");
/// assert_eq!(s.to_class(), "text-black");
/// ```
fn from(color: ColorText) -> Self {
Text::with(color)

View file

@ -0,0 +1,205 @@
use pagetop::prelude::*;
use crate::theme::aux::{ScaleSize, Side};
use crate::theme::BreakPoint;
// **< Margin >*************************************************************************************
/// Clases para establecer **margin** por lado, tamaño y punto de ruptura.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
/// assert_eq!(m.to_class(), "mt-3");
///
/// let m = classes::Margin::with(Side::Start, ScaleSize::Auto).with_breakpoint(BreakPoint::LG);
/// assert_eq!(m.to_class(), "ms-lg-auto");
///
/// let m = classes::Margin::with(Side::All, ScaleSize::None);
/// assert_eq!(m.to_class(), "");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Margin {
side: Side,
size: ScaleSize,
breakpoint: BreakPoint,
}
impl Margin {
/// Crea un **margin** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
/// ruptura.
pub fn with(side: Side, size: ScaleSize) -> Self {
Margin {
side,
size,
breakpoint: BreakPoint::None,
}
}
// **< Margin BUILDER >*************************************************************************
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **margin**.
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
self.breakpoint = breakpoint;
self
}
// **< Margin HELPERS >*************************************************************************
// Devuelve el prefijo `m*` según el lado.
#[rustfmt::skip]
#[inline]
const fn side_prefix(&self) -> &'static str {
match self.side {
Side::All => "m",
Side::Top => "mt",
Side::Bottom => "mb",
Side::Start => "ms",
Side::End => "me",
Side::LeftAndRight => "mx",
Side::TopAndBottom => "my",
}
}
// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
#[rustfmt::skip]
#[inline]
const fn size_suffix(&self) -> Option<&'static str> {
match self.size {
ScaleSize::None => None,
ScaleSize::Auto => Some("auto"),
ScaleSize::Zero => Some("0"),
ScaleSize::One => Some("1"),
ScaleSize::Two => Some("2"),
ScaleSize::Three => Some("3"),
ScaleSize::Four => Some("4"),
ScaleSize::Five => Some("5"),
}
}
/* Añade la clase de **margin** a la cadena de clases (reservado).
//
// No añade nada si `size` es `ScaleSize::None`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let Some(size) = self.size_suffix() else {
return;
};
self.breakpoint
.push_class(classes, self.side_prefix(), size);
} */
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
///
/// Si `size` es `ScaleSize::None`, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let Some(size) = self.size_suffix() else {
return String::new();
};
self.breakpoint.class_with(self.side_prefix(), size)
}
}
// **< Padding >************************************************************************************
/// Clases para establecer **padding** por lado, tamaño y punto de ruptura.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
/// assert_eq!(p.to_class(), "px-2");
///
/// let p = classes::Padding::with(Side::End, ScaleSize::Four).with_breakpoint(BreakPoint::SM);
/// assert_eq!(p.to_class(), "pe-sm-4");
///
/// let p = classes::Padding::with(Side::All, ScaleSize::Auto);
/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding.
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Padding {
side: Side,
size: ScaleSize,
breakpoint: BreakPoint,
}
impl Padding {
/// Crea un **padding** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
/// ruptura.
pub fn with(side: Side, size: ScaleSize) -> Self {
Padding {
side,
size,
breakpoint: BreakPoint::None,
}
}
// **< Padding BUILDER >************************************************************************
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **padding**.
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
self.breakpoint = breakpoint;
self
}
// **< Padding HELPERS >************************************************************************
// Devuelve el prefijo `p*` según el lado.
#[rustfmt::skip]
#[inline]
const fn prefix(&self) -> &'static str {
match self.side {
Side::All => "p",
Side::Top => "pt",
Side::Bottom => "pb",
Side::Start => "ps",
Side::End => "pe",
Side::LeftAndRight => "px",
Side::TopAndBottom => "py",
}
}
// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
//
// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
#[rustfmt::skip]
#[inline]
const fn suffix(&self) -> Option<&'static str> {
match self.size {
ScaleSize::None => None,
ScaleSize::Auto => None,
ScaleSize::Zero => Some("0"),
ScaleSize::One => Some("1"),
ScaleSize::Two => Some("2"),
ScaleSize::Three => Some("3"),
ScaleSize::Four => Some("4"),
ScaleSize::Five => Some("5"),
}
}
/* Añade la clase de **padding** a la cadena de clases (reservado).
//
// No añade nada si `size` es `ScaleSize::None` o `ScaleSize::Auto`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let Some(size) = self.suffix() else {
return;
};
self.breakpoint.push_class(classes, self.prefix(), size);
} */
// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
//
// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let Some(size) = self.suffix() else {
return String::new();
};
self.breakpoint.class_with(self.prefix(), size)
}
}

View file

@ -2,8 +2,6 @@ use pagetop::prelude::*;
use crate::theme::aux::RoundedRadius;
use std::fmt;
/// Clases para definir **esquinas redondeadas**.
///
/// Permite:
@ -20,28 +18,28 @@ use std::fmt;
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::with(RoundedRadius::Default);
/// assert_eq!(r.to_string(), "rounded");
/// assert_eq!(r.to_class(), "rounded");
/// ```
///
/// **Sin redondeo:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new();
/// assert_eq!(r.to_string(), "");
/// assert_eq!(r.to_class(), "");
/// ```
///
/// **Radio en las esquinas de un lado lógico:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
/// assert_eq!(r.to_string(), "rounded-end-2");
/// assert_eq!(r.to_class(), "rounded-end-2");
/// ```
///
/// **Radio en una esquina concreta:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
/// assert_eq!(r.to_string(), "rounded-top-start-3");
/// assert_eq!(r.to_class(), "rounded-top-start-3");
/// ```
///
/// **Combinado (ejemplo completo):**
@ -52,10 +50,10 @@ use std::fmt;
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
///
/// assert_eq!(r.to_string(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Rounded {
radius : RoundedRadius,
top : RoundedRadius,
@ -136,28 +134,36 @@ impl Rounded {
self.bottom_end = radius;
self
}
}
impl fmt::Display for Rounded {
// **< Rounded HELPERS >************************************************************************
/// Añade las clases de redondeo a la cadena de clases.
///
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// `top-start`, `top-end`, `bottom-start` y `bottom-end`; respetando LTR/RTL y omitiendo las
/// definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
[
self.radius.to_string(),
self.top.to_class("rounded-top"),
self.end.to_class("rounded-end"),
self.bottom.to_class("rounded-bottom"),
self.start.to_class("rounded-start"),
self.top_start.to_class("rounded-top-start"),
self.top_end.to_class("rounded-top-end"),
self.bottom_start.to_class("rounded-bottom-start"),
self.bottom_end.to_class("rounded-bottom-end"),
]
.join_classes()
)
#[rustfmt::skip]
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.radius .push_class(classes, "");
self.top .push_class(classes, "rounded-top");
self.end .push_class(classes, "rounded-end");
self.bottom .push_class(classes, "rounded-bottom");
self.start .push_class(classes, "rounded-start");
self.top_start .push_class(classes, "rounded-top-start");
self.top_end .push_class(classes, "rounded-top-end");
self.bottom_start.push_class(classes, "rounded-bottom-start");
self.bottom_end .push_class(classes, "rounded-bottom-end");
}
/// Devuelve las clases de redondeo como cadena (`"rounded"`,
/// `"rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"`, etc.).
///
/// Si no se define ningún radio, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}