From 50e4b42fe48395ee2b888f3a91b33fdd9d29f317 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 13 Sep 2026 00:48:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(pagetop):=20A=C3=B1ade=20Margin=20?= =?UTF-8?q?y=20Padding=20nativos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- examples/intro-spacing.rs | 255 ++++++++++++++++++ examples/locale/en-US/intro-spacing.ftl | 19 ++ examples/locale/es-ES/intro-spacing.ftl | 19 ++ .../pagetop-bootsier/src/theme/class.rs | 3 - .../src/theme/class/layout.rs | 211 --------------- .../src/theme/token/layout.rs | 9 +- src/html.rs | 6 + src/html/flex.rs | 84 ------ src/html/flex/container.rs | 2 +- src/html/flex/item.rs | 2 +- src/html/props/definition.rs | 42 ++- src/html/props/op.rs | 27 +- src/html/responsive.rs | 94 +++++++ src/html/spacing.rs | 30 +++ src/html/spacing/margin.rs | 191 +++++++++++++ src/html/spacing/padding.rs | 202 ++++++++++++++ src/html/unit.rs | 7 + 17 files changed, 881 insertions(+), 322 deletions(-) create mode 100644 examples/intro-spacing.rs create mode 100644 examples/locale/en-US/intro-spacing.ftl create mode 100644 examples/locale/es-ES/intro-spacing.ftl delete mode 100644 extensions/pagetop-bootsier/src/theme/class/layout.rs create mode 100644 src/html/responsive.rs create mode 100644 src/html/spacing.rs create mode 100644 src/html/spacing/margin.rs create mode 100644 src/html/spacing/padding.rs diff --git a/examples/intro-spacing.rs b/examples/intro-spacing.rs new file mode 100644 index 00000000..b650208b --- /dev/null +++ b/examples/intro-spacing.rs @@ -0,0 +1,255 @@ +use pagetop::prelude::*; + +include_locales!(LOC from "examples/locale"); + +struct IntroSpacing; + +#[async_trait] +impl Extension for IntroSpacing { + fn dependencies(&self) -> Vec { + vec![&pagetop_bootsier::Bootsier] + } + + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(intro_spacing)) + } +} + +async fn intro_spacing(request: HttpRequest) -> Result { + Page::new(request) + .with_assets(demo_box_styles()) + .with_assets(demo_row_styles()) + .with_assets(demo_code_styles()) + .with_child( + Intro::default() + .with_opening(IntroOpening::Custom) + .with_title(Lc::n("PageTop")) + .with_slogan(Lc::t("spacing_slogan", &LOC)) + .with_button(None::<(Lc, Route)>) + .with_child(Html::with(|cx| { + html! { + p class="intro-text-lead" { + (Lc::t("spacing_note", &LOC).using(cx)) + } + } + })) + .with_child(padding_block()) + .with_child(layout_block()) + .with_child(responsive_block()) + .with_child(combined_block()), + ) + .render() + .await +} + +fn padding_block() -> Block { + let mut block = Block::new().with_title(Lc::t("spacing_block_title_padding", &LOC)); + + let padding_variants: [(&str, UnitValue, &str); 3] = [ + ( + "spacing_title_uniform_small", + UnitValue::RelRem(0.5), + "Padding::new().with_all(UnitValue::RelRem(0.5))", + ), + ( + "spacing_title_uniform_medium", + UnitValue::RelRem(1.0), + "Padding::new().with_all(UnitValue::RelRem(1.0))", + ), + ( + "spacing_title_uniform_large", + UnitValue::RelRem(2.0), + "Padding::new().with_all(UnitValue::RelRem(2.0))", + ), + ]; + for (title_key, size, code) in padding_variants { + block = block + .with_child(caption(Lc::t(title_key, &LOC), Lc::n(code))) + .with_child( + demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))).with_child( + demo_box(box_sample()).with_prop(Padding::new().with_all(size).into()), + ), + ); + } + + block + .with_child(caption( + Lc::t("spacing_title_sides", &LOC), + Lc::n(concat!( + "Padding::new()", + ".with_top(UnitValue::RelRem(0.25))", + ".with_end(UnitValue::RelRem(2.5))", + ".with_bottom(UnitValue::RelRem(1.5))", + ".with_start(UnitValue::RelRem(0.5))", + )), + )) + .with_child( + demo_row(Flex::new()).with_child( + demo_box(box_sample()).with_prop( + Padding::new() + .with_top(UnitValue::RelRem(0.25)) + .with_end(UnitValue::RelRem(2.5)) + .with_bottom(UnitValue::RelRem(1.5)) + .with_start(UnitValue::RelRem(0.5)) + .into(), + ), + ), + ) +} + +fn layout_block() -> Block { + Block::new() + .with_title(Lc::t("spacing_block_title_layout", &LOC)) + .with_child(caption( + Lc::t("spacing_title_margin_gap", &LOC), + Lc::n("Margin::new().with_x(UnitValue::RelRem(1.0))"), + )) + .with_child( + demo_row(Flex::new()) + .with_child(demo_box(box_sample())) + .with_child( + demo_box(box_sample()) + .with_prop(Margin::new().with_x(UnitValue::RelRem(1.0)).into()), + ) + .with_child(demo_box(box_sample())), + ) + .with_child(caption( + Lc::t("spacing_title_margin_center", &LOC), + Lc::n("Margin::new().with_x(UnitValue::Auto)"), + )) + .with_child( + demo_row(Flex::new()).with_child( + demo_box(box_sample()) + .with_prop(Margin::new().with_x(UnitValue::Auto).into()) + .with_prop(PropsOp::flex_item( + FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::RelRem(8.0))), + )), + ), + ) +} + +fn responsive_block() -> Block { + Block::new() + .with_title(Lc::t("spacing_block_title_responsive", &LOC)) + .with_child(caption( + Lc::t("spacing_title_responsive", &LOC), + Lc::n(concat!( + "Padding::new()", + ".with_all(UnitValue::RelRem(0.5))", + ".with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))", + ".with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0))", + )), + )) + .with_child( + demo_row(Flex::new()).with_child( + demo_box(box_sample()).with_prop( + Padding::new() + .with_all(UnitValue::RelRem(0.5)) + .with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0)) + .with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0)) + .into(), + ), + ), + ) +} + +fn combined_block() -> Block { + Block::new() + .with_title(Lc::t("spacing_block_title_combined", &LOC)) + .with_child(caption( + Lc::t("spacing_title_combined", &LOC), + Lc::n(concat!( + "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( + demo_row(Flex::new()).with_child( + Container::new() + .with_prop(PropsOp::add_classes("spacing-demo-box")) + .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::t("spacing_box_card_button", &LOC)) + .with_style(button::Style::Solid(Intent::Warning)), + ), + ), + ) +} + +// **< HELPERS >************************************************************************************ + +// Caja de muestra sin relleno ni margen propios, para que sólo se vea el efecto de `Margin`/ +// `Padding` aplicado en cada demostración. +fn demo_box_styles() -> AssetsOp { + AssetsOp::add_responsive_styles( + None, + "spacing-demo-box", + [ + ("background-color", "#0d6efd"), + ("color", "#fff"), + ("min-width", "3rem"), + ("width", "auto"), + ("max-width", "none"), + ("margin", "0"), + ("padding", "0"), + ("border-radius", "0.375rem"), + ("text-align", "center"), + ], + ) +} + +// Aspecto fijo de las filas de muestra. +fn demo_row_styles() -> AssetsOp { + AssetsOp::add_responsive_styles( + None, + "spacing-demo-row", + [ + ("background-color", "#f1f3f5"), + ("width", "100%"), + ("max-width", "none"), + ("margin", "0 0 1.5rem"), + ("padding", "0.75rem"), + ], + ) +} + +// Evita que los fragmentos de código largos desborden su contenedor. +fn demo_code_styles() -> AssetsOp { + AssetsOp::add_responsive_styles(None, "spacing-demo-code", [("overflow-wrap", "anywhere")]) +} + +// Caja azul de muestra, sin relleno ni margen propios. +fn demo_box(label: Lc) -> Container { + Container::new() + .with_prop(PropsOp::add_classes("spacing-demo-box")) + .with_child(Html::with(move |cx| html! { (label.using(cx)) })) +} + +// Etiqueta genérica reutilizada en la mayoría de cajas de muestra. +fn box_sample() -> Lc { + Lc::t("spacing_box_sample", &LOC) +} + +// Fila de demostración con fondo gris para visualizar los límites de cada caja. +fn demo_row(flex: Flex) -> Container { + Container::new() + .with_prop(PropsOp::add_classes("spacing-demo-row")) + .with_flex(flex) +} + +// Título y fragmento de código que introducen cada demostración. +fn caption(title: Lc, code: Lc) -> Html { + Html::with(move |cx| { + html! { + h3 { (title.using(cx)) } + p { code class="spacing-demo-code" { (code.using(cx)) } } + } + }) +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&IntroSpacing).await.run().await +} diff --git a/examples/locale/en-US/intro-spacing.ftl b/examples/locale/en-US/intro-spacing.ftl new file mode 100644 index 00000000..57792706 --- /dev/null +++ b/examples/locale/en-US/intro-spacing.ftl @@ -0,0 +1,19 @@ +spacing_slogan = Native margin and padding with Margin and Padding +spacing_note = Margin and Padding have nothing to do with Flexbox: they apply to any component, whether its container uses Flexbox or not. They are combined with Flex here only to lay out the sample boxes in a row. + +spacing_block_title_padding = Padding +spacing_block_title_layout = Layout +spacing_block_title_responsive = Padding at a breakpoint +spacing_block_title_combined = Margin and padding combined + +spacing_title_uniform_small = Small padding on all four sides +spacing_title_uniform_medium = Medium padding on all four sides +spacing_title_uniform_large = Large padding on all four sides +spacing_title_sides = A different value per side, including the logical start/end sides +spacing_title_margin_gap = The middle box's margin pushes its neighbours away, without using Gap +spacing_title_margin_center = An automatic margin on both sides centers the box in the free space +spacing_title_responsive = Padding grows from md onwards, and again from lg +spacing_title_combined = Outer margin above/below and inner padding on all four sides, on a component with real content + +spacing_box_sample = Content +spacing_box_card_button = Accept diff --git a/examples/locale/es-ES/intro-spacing.ftl b/examples/locale/es-ES/intro-spacing.ftl new file mode 100644 index 00000000..9a8cf507 --- /dev/null +++ b/examples/locale/es-ES/intro-spacing.ftl @@ -0,0 +1,19 @@ +spacing_slogan = Márgenes y relleno nativos con Margin y Padding +spacing_note = Margin y Padding no tienen relación con Flexbox: se aplican sobre cualquier componente, use o no Flexbox su contenedor. Aquí se combinan con Flex sólo para colocar las cajas de muestra en fila. + +spacing_block_title_padding = Rellenos +spacing_block_title_layout = Composición +spacing_block_title_responsive = Relleno por punto de corte +spacing_block_title_combined = Margin y padding combinados + +spacing_title_uniform_small = Relleno pequeño en los cuatro lados +spacing_title_uniform_medium = Relleno medio en los cuatro lados +spacing_title_uniform_large = Relleno grande en los cuatro lados +spacing_title_sides = Un valor distinto para cada lado, incluidos los lados lógicos de inicio y fin +spacing_title_margin_gap = El margen de la caja central empuja a sus vecinas, sin usar Gap +spacing_title_margin_center = Un margen automático en ambos lados centra la caja en el espacio libre +spacing_title_responsive = El relleno crece a partir de md, y de nuevo a partir de lg +spacing_title_combined = Margen exterior arriba/abajo y relleno interno por los cuatro lados, sobre un componente con contenido real + +spacing_box_sample = Contenido +spacing_box_card_button = Aceptar diff --git a/extensions/pagetop-bootsier/src/theme/class.rs b/extensions/pagetop-bootsier/src/theme/class.rs index 44c56d5d..ee51497b 100644 --- a/extensions/pagetop-bootsier/src/theme/class.rs +++ b/extensions/pagetop-bootsier/src/theme/class.rs @@ -21,6 +21,3 @@ pub use border::{Border, BorderColor}; mod rounded; pub use rounded::{Rounded, RoundedRadius}; - -mod layout; -pub use layout::{Margin, Padding}; diff --git a/extensions/pagetop-bootsier/src/theme/class/layout.rs b/extensions/pagetop-bootsier/src/theme/class/layout.rs deleted file mode 100644 index 2e65ae15..00000000 --- a/extensions/pagetop-bootsier/src/theme/class/layout.rs +++ /dev/null @@ -1,211 +0,0 @@ -use pagetop::prelude::*; - -use crate::theme::{BoxSide, BreakPoint, ScaleSize}; - -// **< Margin >************************************************************************************* - -/// Clases para establecer **margin** por lado, tamaño y punto de ruptura. -/// -/// # Ejemplos -/// -/// ```rust -/// use pagetop_bootsier::theme::*; -/// -/// let m = class::Margin::with(BoxSide::Top, ScaleSize::Three); -/// assert_eq!(m.to_class(), "mt-3"); -/// -/// let m = class::Margin::with(BoxSide::Start, ScaleSize::Auto) -/// .with_breakpoint(BreakPoint::LG); -/// assert_eq!(m.to_class(), "ms-lg-auto"); -/// -/// let m = class::Margin::with(BoxSide::All, ScaleSize::None); -/// assert_eq!(m.to_class(), ""); -/// ``` -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub struct Margin { - side: BoxSide, - 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: BoxSide, 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 { - BoxSide::All => "m", - BoxSide::Top => "mt", - BoxSide::Bottom => "mb", - BoxSide::Start => "ms", - BoxSide::End => "me", - BoxSide::LeftAndRight => "mx", - BoxSide::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. - pub fn push_to(self, classes: &mut String) { - if let Some(size) = self.size_suffix() { - let side = self.side_prefix(); - self.breakpoint.push_to(classes, side, size); - } - } - - /// Devuelve la clase de *margin* como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.). - /// - /// Si `size` es `ScaleSize::None`, devuelve `""`. - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_to(&mut class); - class - } -} - -impl From for CowStr { - /// Permite pasar [`Margin`] directamente a [`PropsOp`]. - fn from(val: Margin) -> Self { - val.to_class().into() - } -} - -// **< Padding >************************************************************************************ - -/// Clases para establecer **padding** por lado, tamaño y punto de ruptura. -/// -/// # Ejemplos -/// -/// ```rust -/// use pagetop_bootsier::theme::*; -/// -/// let p = class::Padding::with(BoxSide::LeftAndRight, ScaleSize::Two); -/// assert_eq!(p.to_class(), "px-2"); -/// -/// let p = class::Padding::with(BoxSide::End, ScaleSize::Four) -/// .with_breakpoint(BreakPoint::SM); -/// assert_eq!(p.to_class(), "pe-sm-4"); -/// -/// let p = class::Padding::with(BoxSide::All, ScaleSize::Auto); -/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding. -/// ``` -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub struct Padding { - side: BoxSide, - 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: BoxSide, 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 side_prefix(&self) -> &'static str { - match self.side { - BoxSide::All => "p", - BoxSide::Top => "pt", - BoxSide::Bottom => "pb", - BoxSide::Start => "ps", - BoxSide::End => "pe", - BoxSide::LeftAndRight => "px", - BoxSide::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 size_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. - pub fn push_to(self, classes: &mut String) { - if let Some(size) = self.size_suffix() { - let side = self.side_prefix(); - self.breakpoint.push_to(classes, side, size); - } - } - - /// Devuelve la clase de *padding* como cadena (`"px-2"`, `"pe-sm-4"`, etc.). - /// - /// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`. - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_to(&mut class); - class - } -} - -impl From for CowStr { - /// Permite pasar [`Padding`] directamente a [`PropsOp`]. - fn from(val: Padding) -> Self { - val.to_class().into() - } -} diff --git a/extensions/pagetop-bootsier/src/theme/token/layout.rs b/extensions/pagetop-bootsier/src/theme/token/layout.rs index 69401e5d..450c8432 100644 --- a/extensions/pagetop-bootsier/src/theme/token/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/token/layout.rs @@ -4,12 +4,9 @@ use pagetop::prelude::*; /// Escala discreta de tamaños para clases utilitarias. /// -/// Se usa como parámetro de tamaño para las clases de [`Border`], [`Margin`] y [`Padding`]. La -/// variante `Auto` no aplica en `Padding`. +/// Se usa como parámetro de tamaño para las clases de [`Border`]. /// /// [`Border`]: crate::theme::class::Border -/// [`Margin`]: crate::theme::class::Margin -/// [`Padding`]: crate::theme::class::Padding #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum ScaleSize { /// Sin tamaño (no define ninguna clase). @@ -68,11 +65,9 @@ impl ScaleSize { /// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL). /// -/// Se usa como selector de lado para las clases de [`Border`], [`Margin`] y [`Padding`]. +/// Se usa como selector de lado para las clases de [`Border`]. /// /// [`Border`]: crate::theme::class::Border -/// [`Margin`]: crate::theme::class::Margin -/// [`Padding`]: crate::theme::class::Padding #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum BoxSide { /// Todos los lados. diff --git a/src/html.rs b/src/html.rs index 25e03603..80881899 100644 --- a/src/html.rs +++ b/src/html.rs @@ -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}; diff --git a/src/html/flex.rs b/src/html/flex.rs index a810bded..6f4c1b5c 100644 --- a/src/html/flex.rs +++ b/src/html/flex.rs @@ -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, - 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` 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; diff --git a/src/html/flex/container.rs b/src/html/flex/container.rs index 6fd00aab..52d9ebd9 100644 --- a/src/html/flex/container.rs +++ b/src/html/flex/container.rs @@ -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 diff --git a/src/html/flex/item.rs b/src/html/flex/item.rs index e512a4f6..bec22bba 100644 --- a/src/html/flex/item.rs +++ b/src/html/flex/item.rs @@ -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"); diff --git a/src/html/props/definition.rs b/src/html/props/definition.rs index 9c218510..1c7d9091 100644 --- a/src/html/props/definition.rs +++ b/src/html/props/definition.rs @@ -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, diff --git a/src/html/props/op.rs b/src/html/props/op.rs index d67c2e78..4f703162 100644 --- a/src/html/props/op.rs +++ b/src/html/props/op.rs @@ -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) + } } diff --git a/src/html/responsive.rs b/src/html/responsive.rs new file mode 100644 index 00000000..4d0b8bfe --- /dev/null +++ b/src/html/responsive.rs @@ -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 `` 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, + 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` 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; diff --git a/src/html/spacing.rs b/src/html/spacing.rs new file mode 100644 index 00000000..4fb43451 --- /dev/null +++ b/src/html/spacing.rs @@ -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; diff --git a/src/html/spacing/margin.rs b/src/html/spacing/margin.rs new file mode 100644 index 00000000..a293302a --- /dev/null +++ b/src/html/spacing/margin.rs @@ -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, + /// Devuelve el margen inferior, por punto de corte. + #[getters(copy)] + bottom: Responsive, + /// Devuelve el margen del lado lógico de inicio, por punto de corte. + #[getters(copy)] + start: Responsive, + /// Devuelve el margen del lado lógico de fin, por punto de corte. + #[getters(copy)] + end: Responsive, +} + +#[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 for PropsOp { + fn from(margin: Margin) -> Self { + Self::margin(margin) + } +} diff --git a/src/html/spacing/padding.rs b/src/html/spacing/padding.rs new file mode 100644 index 00000000..e0211cdc --- /dev/null +++ b/src/html/spacing/padding.rs @@ -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, + /// Devuelve el relleno interno inferior, por punto de corte. + #[getters(copy)] + bottom: Responsive, + /// Devuelve el relleno interno del lado lógico de inicio, por punto de corte. + #[getters(copy)] + start: Responsive, + /// Devuelve el relleno interno del lado lógico de fin, por punto de corte. + #[getters(copy)] + end: Responsive, +} + +#[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, + 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 for PropsOp { + fn from(padding: Padding) -> Self { + Self::padding(padding) + } +} diff --git a/src/html/unit.rs b/src/html/unit.rs index 8a8f0d0e..65bd7e39 100644 --- a/src/html/unit.rs +++ b/src/html/unit.rs @@ -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.