✨ (pagetop): Añade Margin y Padding nativos

- Nuevo módulo `html::spacing` con `Margin`/`Padding`: igual que
  `FlexItem`, se resuelven generando clases CSS dinámicamente y se
  aplican con `PropsOp::margin()`/`PropsOp::padding()` sobre cualquier
  componente.
- Elimina `Margin`/`Padding` de `pagetop-bootsier`.
- Añade el ejemplo `intro-spacing` con los patrones de uso típicos.
This commit is contained in:
Manuel Cillero 2026-09-13 00:48:11 +02:00
parent 64113aff09
commit 50e4b42fe4
17 changed files with 881 additions and 322 deletions

View file

@ -37,6 +37,12 @@ pub use unit::UnitValue;
// **< HTML LAYOUT >********************************************************************************
mod responsive;
pub mod flex;
#[doc(inline)]
pub use flex::{Flex, FlexItem};
pub mod spacing;
#[doc(inline)]
pub use spacing::{Margin, Padding};

View file

@ -26,10 +26,6 @@
//! [`Container`]: crate::base::component::Container
//! [`Navbar`]: crate::base::component::Navbar
use crate::CowStr;
use crate::core::component::{AssetsOp, Context, Contextual};
use crate::core::theme::BreakpointEntry;
mod props_container;
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
@ -41,83 +37,3 @@ pub use container::Flex;
mod item;
pub use item::FlexItem;
// **< Flex / FlexItem PRIVATE >********************************************************************
// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría
// como `"33_3333pct"`.
fn value_to_token(value: &str) -> String {
value.replace('.', "_").replace('%', "pct")
}
// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada
// con un espacio de las que ya hubiera. Recibe un `BreakpointEntry` ya resuelto (ver
// `Breakpoint::resolved()`) y extrae aquí el `Breakpoint` que `AddResponsiveStyle` necesita, que
// puede ser nulo si aplica siempre.
//
// La clase se copia al acumulador y se mueve al `AssetsOp`, sin clonarla. Recibirla como `CowStr`
// permite además que las clases fijas, las que no dependen de ningún punto de corte, lleguen como
// `&'static str` sin asignar memoria.
fn styles(
cx: &mut Context,
classes: &mut String,
entry: Option<BreakpointEntry>,
class: CowStr,
property: &'static str,
value: CowStr,
) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(&class);
cx.alter_assets(AssetsOp::add_responsive_style(
entry.map(|e| e.breakpoint),
class,
property,
value,
));
}
// Nombre de clase según el punto de corte: `prefix` ya incluye el guion bajo final antes del valor
// (p. ej. `"_flex-direction_"`), y `entry` añade su sufijo si aplica (`"_flex-direction_row_md_"`),
// ya resuelto para el tema activo (ver `Breakpoint::resolved()`).
macro_rules! responsive_class {
($prefix:expr, $token:expr, $entry:expr) => {
match $entry {
None => util::join!($prefix, $token, "_"),
Some(entry) => util::join!($prefix, $token, "_", entry.name, "_"),
}
};
}
use responsive_class;
// Recorre las entradas para una propiedad `Responsive<T>` cuyo valor CSS es un único `T::value()`,
// generando y registrando (vía `styles()`) una clase por punto de corte con valor.
//
// La forma con el marcador final `val` es para propiedades cuyo valor puede contener `.`/`%` (como
// `ItemSize` o `ItemOffset` en `FlexItem`) y necesitan pasar por `value_to_token()`.
macro_rules! apply {
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal) => {
for (bp, value) in $field.by_breakpoint() {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved($cx);
let class = responsive_class!($prefix, value, entry);
styles($cx, $classes, entry, class.into(), $property, value);
}
}
};
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal, val) => {
for (bp, value) in $field.by_breakpoint() {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved($cx);
let class = responsive_class!($prefix, value_to_token(&value), entry);
styles($cx, $classes, entry, class.into(), $property, value);
}
}
};
}
use apply;

View file

@ -238,7 +238,7 @@ impl Flex {
return;
};
use super::{apply, responsive_class, styles, value_to_token};
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
let (prefix, value) = match display {
DisplayFlex::Always

View file

@ -214,7 +214,7 @@ impl FlexItem {
/// cadenas intermedias.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
use super::{apply, responsive_class, styles, value_to_token};
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
apply!(cx, classes, self.grow, "_flex-item-grow_", "flex-grow");
apply!(cx, classes, self.shrink, "_flex-item-shrink_", "flex-shrink");

View file

@ -3,6 +3,7 @@ use crate::core::component::Context;
use crate::html::flex::{Flex, FlexItem};
use crate::html::maud::{Escaper, RenderAttrs};
use crate::html::props::{PropsError, PropsExtra, PropsOp};
use crate::html::spacing::{Margin, Padding};
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
use std::collections::HashMap;
@ -190,6 +191,8 @@ pub struct Props {
attrs: Vec<(CowStr, CowStr)>,
extras: HashMap<&'static str, PropsExtra>,
flex_item: FlexItem,
margin: Margin,
padding: Padding,
}
#[builder_impl]
@ -213,7 +216,8 @@ impl Props {
}
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
/// indicada. El método recomendado para construir cada operación es usar los constructores de
/// indicada, incluido el posicionamiento Flexbox y el espaciado (`FlexItem`, `Margin`,
/// `Padding`). El método recomendado para construir cada operación es usar los constructores de
/// [`PropsOp`].
pub fn with_prop(mut self, op: PropsOp) -> Self {
match op {
@ -339,6 +343,12 @@ impl Props {
PropsOp::FlexItem(placement) => {
self.flex_item = self.flex_item.merge(placement);
}
PropsOp::Margin(margin) => {
self.margin = self.margin.merge(margin);
}
PropsOp::Padding(padding) => {
self.padding = self.padding.merge(padding);
}
}
self
}
@ -560,9 +570,9 @@ impl Props {
///
/// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context`
/// vigente en el punto donde se renderiza, aunque no lo necesite ningún atributo propio. Recibe
/// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelve
/// [`FlexItem`]: las clases que devuelve se añaden a las del propio componente al escribir el
/// atributo `class`.
/// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelven
/// [`FlexItem`], [`Margin`] y [`Padding`]: las clases que devuelven se añaden a las del propio
/// componente al escribir el atributo `class`.
///
/// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`]
/// en su lugar.
@ -578,10 +588,14 @@ impl Props {
/// [`html!`]: crate::html::html
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`Margin`]: crate::html::spacing::Margin
/// [`Padding`]: crate::html::spacing::Padding
/// [`unpack_with_flex()`]: Self::unpack_with_flex
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
let mut classes = String::new();
self.flex_item.apply(cx, &mut classes);
self.margin.apply(cx, &mut classes);
self.padding.apply(cx, &mut classes);
PropsUnpack {
props: self,
classes,
@ -590,20 +604,23 @@ impl Props {
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
///
/// A diferencia de [`FlexItem`] (que se acumula con [`PropsOp::FlexItem`] porque cualquier
/// componente ajeno puede necesitarlo sin tener un campo propio para ello), `Flex` sólo tiene
/// sentido en los contenedores que ya declaran su propio campo `flex: Flex` (`Container`,
/// `Navbar`...): se les pasa aquí directamente, ya resuelto (`self.flex()`), sin pasar por
/// `PropsOp`.
/// A diferencia de [`FlexItem`]/[`Margin`]/[`Padding`] (que se acumulan con sus respectivas
/// variantes de `PropsOp` porque cualquier componente ajeno puede necesitarlos sin tener un
/// campo propio para ello), `Flex` sólo tiene sentido en los contenedores que ya declaran su
/// propio campo `flex: Flex` (como `Container` o `Navbar`). Se les pasa aquí directamente, ya
/// resuelto (`self.flex()`), sin pasar por `PropsOp`.
///
/// [`unpack()`]: Self::unpack
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
/// [`Margin`]: crate::html::spacing::Margin
/// [`Padding`]: crate::html::spacing::Padding
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
let mut classes = String::new();
flex.apply(cx, &mut classes);
self.flex_item.apply(cx, &mut classes);
self.margin.apply(cx, &mut classes);
self.padding.apply(cx, &mut classes);
PropsUnpack {
props: self,
classes,
@ -800,8 +817,9 @@ impl Props {
// **< PropsUnpack >********************************************************************************
// Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas
// por `FlexItem::apply()`/`Flex::apply()` (desde el propio `unpack*()` usando el `&mut Context`),
// pendientes sólo de añadir a las del componente (ver `Props::write_attrs()`).
// por `Flex::apply()`/`FlexItem::apply()`/`Margin::apply()`/`Padding::apply()` (desde el propio
// `unpack*()` usando el `&mut Context`), pendientes sólo de añadir a las del componente (ver
// `Props::write_attrs()`).
struct PropsUnpack<'a> {
props: &'a Props,
classes: String,

View file

@ -2,6 +2,7 @@ use crate::CowStr;
use crate::core::TypeInfo;
use crate::html::flex::FlexItem;
use crate::html::props::extra::PropsExtra;
use crate::html::spacing::{Margin, Padding};
use std::any::Any;
use std::sync::Arc;
@ -40,8 +41,9 @@ use std::sync::Arc;
/// se interpretan como si
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
///
/// Finalmente, [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem sobre
/// cualquier componente.
/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem, mientras que
/// [`Margin`](Self::Margin) y [`Padding`](Self::Padding) aplican márgenes y relleno interno; los
/// tres sobre cualquier componente.
#[derive(Clone, Debug)]
pub enum PropsOp {
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
@ -113,7 +115,7 @@ pub enum PropsOp {
/// Elimina el valor extra asociado a la clave indicada, si existe.
RemoveExtra(&'static str),
/// Aplica un posicionamiento [`FlexItem`] a un componente particular en un contenedor [`Flex`].
/// Añade directamente sus estilos Flexbox al propio componente, sin usar clases CSS.
/// Añade directamente sus estilos Flexbox al propio componente, generando clases CSS dinámicas.
///
/// Existe como variante de `PropsOp`, y no como método builder de un componente, porque las
/// propiedades de un ítem Flexbox tienen sentido sobre **cualquier** componente que pueda
@ -128,6 +130,15 @@ pub enum PropsOp {
/// [`Flex`]: crate::html::flex::Flex
/// [`Container::flex()`]: crate::base::component::Container::flex
FlexItem(FlexItem),
/// Aplica un margen [`Margin`] a un componente particular. Añade directamente sus estilos al
/// propio componente, generando clases CSS dinámicas.
///
/// Como [`FlexItem`](Self::FlexItem), existe como variante de `PropsOp` y no como método
/// builder de un componente, porque el margen tiene sentido sobre **cualquier** componente.
Margin(Margin),
/// Aplica un relleno interno [`Padding`] a un componente particular, siguiendo los mismos
/// criterios que [`Margin`](Self::Margin).
Padding(Padding),
}
impl PropsOp {
@ -269,4 +280,14 @@ impl PropsOp {
pub fn flex_item(placement: FlexItem) -> Self {
Self::FlexItem(placement)
}
/// Crea la variante [`Margin`](Self::Margin) con el margen indicado.
pub fn margin(margin: Margin) -> Self {
Self::Margin(margin)
}
/// Crea la variante [`Padding`](Self::Padding) con el relleno interno indicado.
pub fn padding(padding: Padding) -> Self {
Self::Padding(padding)
}
}

94
src/html/responsive.rs Normal file
View file

@ -0,0 +1,94 @@
//! Mecanismo interno compartido para resolver clases CSS nativas *responsive*.
//!
//! Usado por [`flex`] y [`spacing`]. Ambos módulos resuelven su configuración generando clases CSS
//! dinámicamente, de manera independiente a cualquier tema o framework CSS, registradas vía
//! [`AssetsOp::add_responsive_style()`] y renderizadas como reglas en el `<head>` del documento.
//! El nombre interno de cada clase se deriva de la propiedad y el valor que representa, así que dos
//! elementos con la misma configuración comparten la misma regla en vez de duplicarla.
//!
//! [`flex`]: crate::html::flex
//! [`spacing`]: crate::html::spacing
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
use crate::CowStr;
use crate::core::component::{AssetsOp, Context, Contextual};
use crate::core::theme::BreakpointEntry;
// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría
// como `"33_3333pct"`.
pub(crate) fn value_to_token(value: &str) -> String {
value.replace('.', "_").replace('%', "pct")
}
// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada
// con un espacio de las que ya hubiera. Recibe un `BreakpointEntry` ya resuelto (ver
// `Breakpoint::resolved()`) y extrae aquí el `Breakpoint` que `AddResponsiveStyle` necesita, que
// puede ser nulo si aplica siempre.
//
// La clase se copia al acumulador y se mueve al `AssetsOp`, sin clonarla. Recibirla como `CowStr`
// permite además que las clases fijas, las que no dependen de ningún punto de corte, lleguen como
// `&'static str` sin asignar memoria.
pub(crate) fn styles(
cx: &mut Context,
classes: &mut String,
entry: Option<BreakpointEntry>,
class: CowStr,
property: &'static str,
value: CowStr,
) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(&class);
cx.alter_assets(AssetsOp::add_responsive_style(
entry.map(|e| e.breakpoint),
class,
property,
value,
));
}
// Nombre de clase según el punto de corte: `prefix` ya incluye el guion bajo final antes del valor
// (p. ej. `"_flex-direction_"`), y `entry` añade su sufijo si aplica (`"_flex-direction_row_md_"`),
// ya resuelto para el tema activo (ver `Breakpoint::resolved()`).
macro_rules! responsive_class {
($prefix:expr, $token:expr, $entry:expr) => {
match $entry {
None => util::join!($prefix, $token, "_"),
Some(entry) => util::join!($prefix, $token, "_", entry.name, "_"),
}
};
}
pub(crate) use responsive_class;
// Recorre las entradas para una propiedad `Responsive<T>` cuyo valor CSS es un único `T::value()`,
// generando y registrando (vía `styles()`) una clase por punto de corte con valor.
//
// La forma con el marcador final `val` es para propiedades cuyo valor puede contener `.`/`%` (como
// `ItemSize`/`ItemOffset` en `FlexItem`, o `UnitValue` en `Margin`/`Padding`) y necesitan pasar por
// `value_to_token()`.
macro_rules! apply {
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal) => {
for (bp, value) in $field.by_breakpoint() {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved($cx);
let class = responsive_class!($prefix, value, entry);
styles($cx, $classes, entry, class.into(), $property, value);
}
}
};
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal, val) => {
for (bp, value) in $field.by_breakpoint() {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved($cx);
let class = responsive_class!($prefix, value_to_token(&value), entry);
styles($cx, $classes, entry, class.into(), $property, value);
}
}
};
}
pub(crate) use apply;

30
src/html/spacing.rs Normal file
View file

@ -0,0 +1,30 @@
//! Márgenes y relleno nativos aplicados a componentes.
//!
//! [`Margin`] y [`Padding`] configuran márgenes externos y relleno interno por lado lógico y punto
//! de corte. Aplican sobre cualquier componente. Se usan igual que [`FlexItem`], con
//! [`PropsOp::margin()`] y [`PropsOp::padding()`] sobre el `with_prop()` que normalmente ya expone
//! cualquier componente.
//!
//! # Ejemplo
//!
//! ```rust,no_run
//! use pagetop::prelude::*;
//!
//! // Margen exterior arriba/abajo y relleno interno por los cuatro lados.
//! // `Margin` y `Padding` implementan `From` para `PropsOp`, así que `with_prop()`
//! // acepta `.into()` en vez de `PropsOp::margin()`/`PropsOp::padding()`.
//! let card = Container::new()
//! .with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)).into())
//! .with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)).into())
//! .with_child(Button::plain(Lc::n("Aceptar")));
//! ```
//!
//! [`FlexItem`]: crate::html::flex::FlexItem
//! [`PropsOp::margin()`]: crate::html::props::PropsOp::margin
//! [`PropsOp::padding()`]: crate::html::props::PropsOp::padding
mod margin;
pub use margin::Margin;
mod padding;
pub use padding::Padding;

191
src/html/spacing/margin.rs Normal file
View file

@ -0,0 +1,191 @@
use crate::core::component::Context;
use crate::core::theme::{Breakpoint, Responsive};
use crate::html::PropsOp;
use crate::html::unit::UnitValue;
use crate::{AutoDefault, Getters, builder_impl, util};
/// Configuración de márgenes externos por lado lógico y punto de corte.
///
/// No tiene relación con Flexbox. Se aplica sobre cualquier componente, con [`PropsOp::margin()`]
/// desde el `with_prop()` que suele exponer cualquier componente.
///
/// Cada lado admite cualquier [`UnitValue`], incluido [`UnitValue::Auto`] (por ejemplo, para
/// centrar un bloque con `margin-inline: auto`). Los lados lógicos `start`/`end` se traducen a
/// `margin-inline-start`/`margin-inline-end`, respetando LTR/RTL.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// // Centra el bloque horizontalmente y añade espacio inferior.
/// let panel = Container::new().with_prop(PropsOp::margin(
/// Margin::new()
/// .with_x(UnitValue::Auto)
/// .with_bottom(UnitValue::RelRem(1.5)),
/// ));
/// ```
///
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct Margin {
/// Devuelve el margen superior, por punto de corte.
#[getters(copy)]
top: Responsive<UnitValue>,
/// Devuelve el margen inferior, por punto de corte.
#[getters(copy)]
bottom: Responsive<UnitValue>,
/// Devuelve el margen del lado lógico de inicio, por punto de corte.
#[getters(copy)]
start: Responsive<UnitValue>,
/// Devuelve el margen del lado lógico de fin, por punto de corte.
#[getters(copy)]
end: Responsive<UnitValue>,
}
#[builder_impl]
impl Margin {
/// Crea una configuración de margen sin ningún lado establecido.
pub fn new() -> Self {
Self::default()
}
// **< Margin BUILDER >*************************************************************************
/// Establece el margen superior.
pub fn with_top(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self
}
/// Establece el margen superior, a partir del punto de corte indicado.
pub fn with_top_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self
}
/// Establece el margen inferior.
pub fn with_bottom(mut self, value: UnitValue) -> Self {
self.bottom = self.bottom.set(value);
self
}
/// Establece el margen inferior, a partir del punto de corte indicado.
pub fn with_bottom_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.bottom = self.bottom.set_at(bp, value);
self
}
/// Establece el margen del lado lógico de inicio (`margin-inline-start`).
pub fn with_start(mut self, value: UnitValue) -> Self {
self.start = self.start.set(value);
self
}
/// Establece el margen del lado lógico de inicio (`margin-inline-start`), a partir del punto de
/// corte indicado.
pub fn with_start_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.start = self.start.set_at(bp, value);
self
}
/// Establece el margen del lado lógico de fin (`margin-inline-end`).
pub fn with_end(mut self, value: UnitValue) -> Self {
self.end = self.end.set(value);
self
}
/// Establece el margen del lado lógico de fin (`margin-inline-end`), a partir del punto de
/// corte indicado.
pub fn with_end_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.end = self.end.set_at(bp, value);
self
}
/// Establece el mismo margen en ambos lados lógicos laterales (inicio y fin).
pub fn with_x(mut self, value: UnitValue) -> Self {
self.start = self.start.set(value);
self.end = self.end.set(value);
self
}
/// Establece el mismo margen en ambos lados lógicos laterales (inicio y fin), a partir del
/// punto de corte indicado.
pub fn with_x_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.start = self.start.set_at(bp, value);
self.end = self.end.set_at(bp, value);
self
}
/// Establece el mismo margen arriba y abajo.
pub fn with_y(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self.bottom = self.bottom.set(value);
self
}
/// Establece el mismo margen arriba y abajo, a partir del punto de corte indicado.
pub fn with_y_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self.bottom = self.bottom.set_at(bp, value);
self
}
/// Establece el mismo margen en los cuatro lados.
pub fn with_all(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self.bottom = self.bottom.set(value);
self.start = self.start.set(value);
self.end = self.end.set(value);
self
}
/// Establece el mismo margen en los cuatro lados, a partir del punto de corte indicado.
pub fn with_all_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self.bottom = self.bottom.set_at(bp, value);
self.start = self.start.set_at(bp, value);
self.end = self.end.set_at(bp, value);
self
}
}
impl Margin {
/// Combina esta configuración con otra `Margin`, lado a lado y punto de corte a punto de corte.
/// Donde `margin` tenga un valor establecido, sustituye al de `self`, y donde no lo tenga, se
/// conserva el de `self`. Es el método que usa [`Props::with_prop()`] para que sucesivas
/// [`PropsOp::Margin`] sobre el mismo componente vayan completando lados concretos sin repetir
/// los ya establecidos.
///
/// [`Props::with_prop()`]: crate::html::Props::with_prop
/// [`PropsOp::Margin`]: crate::html::PropsOp::Margin
pub fn merge(mut self, margin: Margin) -> Self {
self.top = self.top.merge(margin.top);
self.bottom = self.bottom.merge(margin.bottom);
self.start = self.start.merge(margin.start);
self.end = self.end.merge(margin.end);
self
}
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
/// [`FlexItem::apply()`](crate::html::flex::FlexItem::apply). Un lado sin ningún valor
/// establecido, o con [`UnitValue::None`], no añade nada.
///
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
apply!(cx, classes, self.top, "_margin-top_", "margin-top", val);
apply!(cx, classes, self.bottom, "_margin-bottom_", "margin-bottom", val);
apply!(cx, classes, self.start, "_margin-start_", "margin-inline-start", val);
apply!(cx, classes, self.end, "_margin-end_", "margin-inline-end", val);
}
}
impl From<Margin> for PropsOp {
fn from(margin: Margin) -> Self {
Self::margin(margin)
}
}

202
src/html/spacing/padding.rs Normal file
View file

@ -0,0 +1,202 @@
use crate::core::component::Context;
use crate::core::theme::{Breakpoint, Responsive};
use crate::html::PropsOp;
use crate::html::unit::UnitValue;
use crate::{AutoDefault, Getters, builder_impl, util};
/// Configuración de relleno interno por lado lógico y punto de corte.
///
/// Mismo mecanismo y criterio de uso que [`Margin`](super::Margin): no tiene relación con Flexbox,
/// y se aplica sobre cualquier componente vía [`PropsOp::padding()`] desde su `with_prop()`.
///
/// A diferencia de `Margin`, [`UnitValue::Auto`] no tiene efecto en ningún lado ya que CSS no
/// admite `padding: auto`, así que un lado establecido a `Auto` se ignora como si no se hubiera
/// establecido.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let card = Container::new().with_prop(PropsOp::padding(
/// Padding::new()
/// .with_all(UnitValue::RelRem(1.0))
/// .with_bottom_at(Breakpoint::Md, UnitValue::RelRem(2.0)),
/// ));
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct Padding {
/// Devuelve el relleno interno superior, por punto de corte.
#[getters(copy)]
top: Responsive<UnitValue>,
/// Devuelve el relleno interno inferior, por punto de corte.
#[getters(copy)]
bottom: Responsive<UnitValue>,
/// Devuelve el relleno interno del lado lógico de inicio, por punto de corte.
#[getters(copy)]
start: Responsive<UnitValue>,
/// Devuelve el relleno interno del lado lógico de fin, por punto de corte.
#[getters(copy)]
end: Responsive<UnitValue>,
}
#[builder_impl]
impl Padding {
/// Crea una configuración de relleno sin ningún lado establecido.
pub fn new() -> Self {
Self::default()
}
// **< Padding BUILDER >************************************************************************
/// Establece el relleno interno superior.
pub fn with_top(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self
}
/// Establece el relleno interno superior, a partir del punto de corte indicado.
pub fn with_top_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self
}
/// Establece el relleno interno inferior.
pub fn with_bottom(mut self, value: UnitValue) -> Self {
self.bottom = self.bottom.set(value);
self
}
/// Establece el relleno interno inferior, a partir del punto de corte indicado.
pub fn with_bottom_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.bottom = self.bottom.set_at(bp, value);
self
}
/// Establece el relleno interno del lado lógico de inicio (`padding-inline-start`).
pub fn with_start(mut self, value: UnitValue) -> Self {
self.start = self.start.set(value);
self
}
/// Establece el relleno interno del lado lógico de inicio (`padding-inline-start`), a partir
/// del punto de corte indicado.
pub fn with_start_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.start = self.start.set_at(bp, value);
self
}
/// Establece el relleno interno del lado lógico de fin (`padding-inline-end`).
pub fn with_end(mut self, value: UnitValue) -> Self {
self.end = self.end.set(value);
self
}
/// Establece el relleno interno del lado lógico de fin (`padding-inline-end`), a partir del
/// punto de corte indicado.
pub fn with_end_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.end = self.end.set_at(bp, value);
self
}
/// Establece el mismo relleno interno en ambos lados lógicos laterales (inicio y fin).
pub fn with_x(mut self, value: UnitValue) -> Self {
self.start = self.start.set(value);
self.end = self.end.set(value);
self
}
/// Establece el mismo relleno interno en ambos lados lógicos laterales (inicio y fin), a partir
/// del punto de corte indicado.
pub fn with_x_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.start = self.start.set_at(bp, value);
self.end = self.end.set_at(bp, value);
self
}
/// Establece el mismo relleno interno arriba y abajo.
pub fn with_y(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self.bottom = self.bottom.set(value);
self
}
/// Establece el mismo relleno interno arriba y abajo, a partir del punto de corte indicado.
pub fn with_y_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self.bottom = self.bottom.set_at(bp, value);
self
}
/// Establece el mismo relleno interno en los cuatro lados.
pub fn with_all(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self.bottom = self.bottom.set(value);
self.start = self.start.set(value);
self.end = self.end.set(value);
self
}
/// Establece el mismo relleno interno en los cuatro lados, a partir del punto de corte
/// indicado.
pub fn with_all_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self.bottom = self.bottom.set_at(bp, value);
self.start = self.start.set_at(bp, value);
self.end = self.end.set_at(bp, value);
self
}
}
impl Padding {
/// Combina esta configuración con otra `Padding`, lado a lado y punto de corte a punto de
/// corte; mismo criterio que [`Margin::merge()`](super::Margin::merge).
pub fn merge(mut self, padding: Padding) -> Self {
self.top = self.top.merge(padding.top);
self.bottom = self.bottom.merge(padding.bottom);
self.start = self.start.merge(padding.start);
self.end = self.end.merge(padding.end);
self
}
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
/// [`Margin::apply()`](super::Margin::apply), salvo que aquí un lado con [`UnitValue::Auto`] se
/// descarta (ver la documentación de este tipo).
///
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
Self::apply_side(cx, classes, self.top, "_padding-top_", "padding-top");
Self::apply_side(cx, classes, self.bottom, "_padding-bottom_", "padding-bottom");
Self::apply_side(cx, classes, self.start, "_padding-start_", "padding-inline-start");
Self::apply_side(cx, classes, self.end, "_padding-end_", "padding-inline-end");
}
// Aplica un único lado, descartando `UnitValue::Auto` porque CSS no admite `padding: auto`.
fn apply_side(
cx: &mut Context,
classes: &mut String,
field: Responsive<UnitValue>,
prefix: &'static str,
property: &'static str,
) {
use crate::html::responsive::{responsive_class, styles, value_to_token};
for (bp, value) in field.by_breakpoint() {
if value != UnitValue::Auto {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved(cx);
let class = responsive_class!(prefix, value_to_token(&value), entry);
styles(cx, classes, entry, class.into(), property, value);
}
}
}
}
}
impl From<Padding> for PropsOp {
fn from(padding: Padding) -> Self {
Self::padding(padding)
}
}

View file

@ -126,6 +126,13 @@ impl UnitValue {
pub const fn is_measurable(&self) -> bool {
!matches!(self, UnitValue::None | UnitValue::Auto)
}
// Convierte a un valor CSS ya formateado. Da a `UnitValue` la misma forma de acceso (`value()`)
// que ya usan propiedades de `html::flex` como `Direction`, `ItemSize`, `ItemOffset`, etc.,
// para poder combinarse con su misma macro (`apply!`).
pub(crate) fn value(self) -> CowStr {
self.into()
}
}
/// Formatea la unidad como cadena CSS.