✨ (pagetop): Añade Grid y extrae Flex de Container

This commit is contained in:
Manuel Cillero 2026-09-18 16:49:09 +02:00
parent dfff12881f
commit 307ca6f62c
44 changed files with 2554 additions and 735 deletions

View file

@ -1,4 +1,4 @@
//! Definiciones de alineación compartidas por [`Flex`] y [`Grid`].
//! Definiciones de alineación compartidas por los contenedores [`Flex`] y [`Grid`].
//!
//! Reúne las definiciones CSS que Flexbox y CSS Grid resuelven de forma idéntica, con el mismo
//! nombre de propiedad y mismo catálogo de valores. La propia especificación CSS no los considera
@ -24,8 +24,8 @@
//! comparte y qué no).
//!
//! [CSS Box Alignment Module]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_alignment
//! [`Flex`]: crate::html::Flex
//! [`Grid`]: crate::html::Grid
//! [`Flex`]: crate::base::component::Flex
//! [`Grid`]: crate::base::component::Grid
//! [`align::Items`]: crate::html::align::Items
//! [`align::Content`]: crate::html::align::Content
//! [`align::ItemSelf`]: crate::html::align::ItemSelf
@ -39,14 +39,14 @@ use crate::{AutoDefault, CowStr};
/// Alinea los elementos en un contenedor [`Flex`] (eje transversal) o [`Grid`] (eje de filas).
///
/// [`Flex`]: crate::html::Flex
/// [`Grid`]: crate::html::Grid
/// [`Flex`]: crate::base::component::Flex
/// [`Grid`]: crate::base::component::Grid
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Items {
/// Por defecto (`align-items: normal` no explícito), mismo efecto que [`Items::Stretch`], salvo
/// que el elemento tenga su propio tamaño.
#[default]
Default,
Normal,
/// Alinea los elementos al inicio del eje (`align-items: flex-start`).
Start,
/// Alinea los elementos al final del eje (`align-items: flex-end`).
@ -63,7 +63,7 @@ impl Items {
// Devuelve el valor CSS de `align-items`, o "" para el valor por defecto.
pub(crate) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Normal => "".into(),
Self::Start => "flex-start".into(),
Self::End => "flex-end".into(),
Self::Center => "center".into(),
@ -84,8 +84,8 @@ impl Items {
/// la suma de las pistas de fila sea menor que la altura del contenedor, sin depender de ningún
/// ajuste de línea.
///
/// [`Flex`]: crate::html::Flex
/// [`Grid`]: crate::html::Grid
/// [`Flex`]: crate::base::component::Flex
/// [`Grid`]: crate::base::component::Grid
/// [`Behavior::Wrap`]: crate::html::flex::Behavior::Wrap
/// [`Behavior::WrapReverse`]: crate::html::flex::Behavior::WrapReverse
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -94,7 +94,7 @@ pub enum Content {
/// o pistas se estiran para ocupar el espacio sobrante, sin efecto visible si no hay ningún
/// espacio sobrante que repartir (p. ej. una altura `auto` ajustada al contenido).
#[default]
Default,
Normal,
/// Alinea al inicio del eje (`align-content: flex-start`).
Start,
/// Alinea al final del eje (`align-content: flex-end`).
@ -116,7 +116,7 @@ impl Content {
// Devuelve el valor CSS de `align-content`, o "" para el valor por defecto.
pub(crate) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Normal => "".into(),
Self::Start => "flex-start".into(),
Self::End => "flex-end".into(),
Self::Center => "center".into(),
@ -139,8 +139,8 @@ impl Content {
/// Esa combinación no aplica a Grid, donde el reparto de espacio entre pistas no tiene el mismo
/// problema.
///
/// [`Flex`]: crate::html::Flex
/// [`Grid`]: crate::html::Grid
/// [`Flex`]: crate::base::component::Flex
/// [`Grid`]: crate::base::component::Grid
/// [`ItemSize`]: crate::html::flex::ItemSize
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Gap {

View file

@ -37,7 +37,7 @@ impl ResponsiveStyles {
/// Si ya existe una declaración para la misma propiedad, en el mismo punto de corte y con las
/// mismas clases, la llamada no hace nada: se conserva el valor ya almacenado, no se sustituye.
/// Pensado para clases utilitarias generadas automáticamente, donde el mismo nombre de clase
/// implica siempre el mismo valor -- declararla de nuevo es entonces una operación de sólo
/// implica siempre el mismo valor. Declararla de nuevo es entonces una operación de sólo
/// lectura, sin normalizar `value`, en vez de una escritura.
///
/// Si `classes` contiene caracteres no ASCII, o si `classes`, `property` o `value` quedan

View file

@ -1,14 +1,16 @@
//! Definiciones para el posicionamiento de componentes con [Flexbox].
//!
//! [`Flex`] configura un contenedor y sus hijos como un grupo sobre el que se aplican propiedades
//! de presentación (dirección, ajuste de línea, alineación, espaciado). Lo usan componentes que
//! ofrecen su propio `with_flex()`, como [`Container`] o [`Navbar`].
//! Por un lado está [`Flex`], el **componente contenedor** disponible en
//! [`pagetop::base::component`] que permite estructurar un conjunto de componentes hijo como un
//! grupo Flexbox sobre el que se aplican propiedades de presentación (dirección, ajuste de línea,
//! alineación, espaciado), con sus propios constructores (`new()`, `at()`, `inline()`,
//! `inline_at()`) y builders (`with_direction()`, `with_gap()`, etc.).
//!
//! [`FlexItem`] configura, en cambio, un único elemento en relación con el contenedor flex de su
//! padre (crecimiento, reducción, alineación individual, orden, ancho y desplazamiento). No tiene
//! un builder propio ya que puede acabar aplicándose sobre cualquier componente (no sólo los que
//! ofrecen `with_flex()`). Por eso se aplica pasándolo directamente al `with_prop()` que
//! normalmente ya expone cualquier componente, gracias a su `From` hacia [`PropsOp`].
//! Por otro, aquí se define [`FlexItem`], que es la **configuración** que se aplica sobre un único
//! elemento respecto a su contenedor flex padre (crecimiento, reducción, alineación individual,
//! orden, ancho y desplazamiento). No tiene un builder propio ya que podría usarse sobre cualquier
//! componente. Por eso se aplica pasándolo directamente al `with_prop()` que normalmente ya expone
//! cualquier componente, gracias a su implementación `From` hacia [`PropsOp`].
//!
//! # Un entorno nativo autosuficiente
//!
@ -20,20 +22,30 @@
//! documento. Funciona igual conviva con quien conviva en la misma página, sin necesidad de
//! coordinar nombres de clase ni orden alguno en la carga de hojas de estilo.
//!
//! # Alineación y espaciado: ver [`align`]
//!
//! `align`/`align_content`/`gap` en [`Flex`] y `align_self` en [`FlexItem`] no tienen tipos propios
//! de este módulo. Usan [`align::Items`], [`align::Content`], [`align::Gap`] y [`align::ItemSelf`],
//! tipos declarados en [`pagetop::html::align`] con las propiedades que Flexbox y CSS Grid
//! resuelven de manera idéntica.
//!
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
//! [`pagetop::html::align`]: crate::html::align
//! [`align`]: crate::html::align
//! [`align::Items`]: crate::html::align::Items
//! [`align::Content`]: crate::html::align::Content
//! [`align::Gap`]: crate::html::align::Gap
//! [`align::ItemSelf`]: crate::html::align::ItemSelf
//! [`PropsOp`]: crate::html::props::PropsOp
//! [`Container`]: crate::base::component::Container
//! [`Navbar`]: crate::base::component::Navbar
//! [`pagetop::base::component`]: crate::base::component
//! [`Flex`]: crate::base::component::Flex
mod props_container;
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
pub use props_container::{Behavior, ContentJustify, Direction};
mod props_item;
pub use props_item::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
mod container;
pub use container::Flex;
pub use props_item::{ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
mod item;
pub use item::FlexItem;

View file

@ -1,278 +0,0 @@
use crate::core::component::Context;
use crate::core::theme::{Breakpoint, Responsive};
use crate::html::flex::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
use crate::{AutoDefault, Getters, builder_impl, util};
// **< DisplayFlex >********************************************************************************
// Modo de activación del posicionamiento Flexbox de un contenedor `Flex`. Detalle interno de
// implementación: la API pública sólo expone los constructores `Flex::new()`, `Flex::at()`,
// `Flex::inline()` e `Flex::inline_at()`, nunca esta variante directamente.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
enum DisplayFlex {
#[default]
Always,
AlwaysInline,
At(Breakpoint),
InlineAt(Breakpoint),
}
// **< Flex >***************************************************************************************
/// Configuración para el posicionamiento Flexbox en un contenedor.
///
/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
/// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía
/// [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en el
/// `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de los
/// temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
///
/// El nombre de cada clase se deriva de la propiedad y el valor que representa (por ejemplo
/// `_flex-direction_row_`), así que dos contenedores con la misma configuración comparten la misma
/// regla generada en vez de duplicarla, y el nombre generado no coincide por accidente con clases
/// de terceros.
///
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let actions = Container::new()
/// .with_flex(
/// Flex::new()
/// .with_justify(flex::ContentJustify::End)
/// .with_align(flex::Align::Center)
/// .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
/// )
/// .with_child(Button::submit(Lc::n("Save")))
/// .with_child(Button::plain(Lc::n("Cancel")));
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct Flex {
// Determina si esta configuración debe aplicarse (y con qué variante de `display`) o si
// `Flex` no está en absoluto configurado. `None` es el estado real de ausencia: lo que tiene
// un contenedor que nunca ha llamado a `with_flex()`. Sin getter público; `new()`, `at()`,
// `inline()` e `inline_at()` son la única forma de activarlo.
#[getters(skip)]
display: Option<DisplayFlex>,
/// Devuelve la dirección del eje principal por punto de corte.
#[getters(copy)]
direction: Responsive<Direction>,
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea, por punto de
/// corte.
#[getters(copy)]
wrap: Responsive<Behavior>,
/// Devuelve la alineación de los elementos en el eje principal, por punto de corte.
#[getters(copy)]
justify: Responsive<ContentJustify>,
/// Devuelve la alineación de los elementos en el eje transversal, por punto de corte.
#[getters(copy)]
align: Responsive<Align>,
/// Devuelve la alineación de las líneas cuando hay más de una, por punto de corte.
#[getters(copy)]
align_content: Responsive<AlignContent>,
/// Devuelve el espaciado entre elementos, por punto de corte.
#[getters(copy)]
gap: Responsive<Gap>,
}
#[builder_impl]
impl Flex {
/// Define una configuración Flex con `display: flex`, sin punto de corte: se aplica siempre.
pub fn new() -> Self {
Self {
display: Some(DisplayFlex::Always),
..Default::default()
}
}
/// Define una configuración Flex con `display: flex` que se aplica a partir del punto de corte
/// indicado.
pub fn at(bp: Breakpoint) -> Self {
Self {
display: Some(DisplayFlex::At(bp)),
..Default::default()
}
}
/// Define una configuración Flex con `display: inline-flex`, sin punto de corte: se aplica
/// siempre.
pub fn inline() -> Self {
Self {
display: Some(DisplayFlex::AlwaysInline),
..Default::default()
}
}
/// Define una configuración Flex con `display: inline-flex` que se aplica a partir del punto de
/// corte indicado.
pub fn inline_at(bp: Breakpoint) -> Self {
Self {
display: Some(DisplayFlex::InlineAt(bp)),
..Default::default()
}
}
// **< Flex BUILDER >***************************************************************************
/// Establece la dirección del eje principal.
pub fn with_direction(mut self, dir: Direction) -> Self {
self.direction = self.direction.set(dir);
self
}
/// Establece la dirección del eje principal a partir del punto de corte indicado.
pub fn with_direction_at(mut self, bp: Breakpoint, dir: Direction) -> Self {
self.direction = self.direction.set_at(bp, dir);
self
}
/// Establece el comportamiento cuando los elementos no caben en una sola línea.
pub fn with_wrap(mut self, wrap: Behavior) -> Self {
self.wrap = self.wrap.set(wrap);
self
}
/// Establece el comportamiento cuando los elementos no caben en una sola línea, a partir del
/// punto de corte indicado.
pub fn with_wrap_at(mut self, bp: Breakpoint, wrap: Behavior) -> Self {
self.wrap = self.wrap.set_at(bp, wrap);
self
}
/// Establece la alineación de los elementos en el eje principal.
pub fn with_justify(mut self, justify: ContentJustify) -> Self {
self.justify = self.justify.set(justify);
self
}
/// Establece la alineación de los elementos en el eje principal, a partir del punto de corte
/// indicado.
pub fn with_justify_at(mut self, bp: Breakpoint, justify: ContentJustify) -> Self {
self.justify = self.justify.set_at(bp, justify);
self
}
/// Establece la alineación de los elementos en el eje transversal.
pub fn with_align(mut self, align: Align) -> Self {
self.align = self.align.set(align);
self
}
/// Establece la alineación de los elementos en el eje transversal, a partir del punto de corte
/// indicado.
pub fn with_align_at(mut self, bp: Breakpoint, align: Align) -> Self {
self.align = self.align.set_at(bp, align);
self
}
/// Establece la alineación de las líneas cuando hay más de una (ver [`AlignContent`]).
pub fn with_align_content(mut self, align_content: AlignContent) -> Self {
self.align_content = self.align_content.set(align_content);
self
}
/// Establece la alineación de las líneas cuando hay más de una (ver [`AlignContent`]), a partir
/// del punto de corte indicado.
pub fn with_align_content_at(mut self, bp: Breakpoint, align_content: AlignContent) -> Self {
self.align_content = self.align_content.set_at(bp, align_content);
self
}
/// Establece el espaciado entre elementos.
pub fn with_gap(mut self, gap: Gap) -> Self {
self.gap = self.gap.set(gap);
self
}
/// Establece el espaciado entre elementos, a partir del punto de corte indicado.
pub fn with_gap_at(mut self, bp: Breakpoint, gap: Gap) -> Self {
self.gap = self.gap.set_at(bp, gap);
self
}
}
impl Flex {
/// Combina esta configuración con otra `Flex`, campo a campo, o la resetea a los valores por
/// defecto si se pasa `None`.
///
/// Cada campo de `flex` que tenga un valor sustituye al correspondiente de `self`; los que
/// estén a `None` dejan intacto el valor ya presente en `self`. Así, sucesivas llamadas pueden
/// ir completando o sobrescribiendo campos concretos sin necesidad de repetir los ya
/// establecidos. Es el método recomendado para que un contenedor propio adopte `Flex` de forma
/// incremental (ver [`Container::with_flex()`](crate::base::component::Container::with_flex)
/// como referencia de uso).
pub fn merge(mut self, flex: impl Into<Option<Flex>>) -> Self {
let Some(flex) = flex.into() else {
return Flex::default();
};
self.display = flex.display.or(self.display);
self.direction = self.direction.merge(flex.direction);
self.wrap = self.wrap.merge(flex.wrap);
self.justify = self.justify.merge(flex.justify);
self.align = self.align.merge(flex.align);
self.align_content = self.align_content.merge(flex.align_content);
self.gap = self.gap.merge(flex.gap);
self
}
/// Aplica esta configuración a un [`Props`] como declaraciones de estilo en línea.
///
/// Es el método recomendado para que un componente adopte `Flex`: concentra en un único sitio
/// la traducción de la configuración a estilos, para no repetirla en cada componente que la
/// use. Precedente: [`Container`](crate::base::component::Container) lo aplica sobre su
/// propio `Props`; [`Navbar`](crate::base::component::Navbar), sobre el `Props` de su área de
/// contenido.
///
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
/// para poder compartir un único acumulador con [`FlexItem::apply()`](super::FlexItem::apply)
/// sin cadenas intermedias.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
// Sin `display` no hay contenedor Flex: el resto de facetas (`flex-direction`, `gap`...)
// no tienen ningún efecto en CSS sin `display: flex`/`inline-flex`, así que ni se generan.
let Some(display) = self.display else {
return;
};
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
let (prefix, value) = match display {
DisplayFlex::Always
| DisplayFlex::At(_) => ("_flex_", "flex"),
DisplayFlex::AlwaysInline
| DisplayFlex::InlineAt(_) => ("_inline-flex_", "inline-flex"),
};
let entry = match display {
DisplayFlex::At(bp) | DisplayFlex::InlineAt(bp) => bp.resolved(cx),
_ => None,
};
let class = match entry {
None => prefix.into(),
Some(entry) => util::join!(prefix, entry.name, "_").into(),
};
styles(cx, classes, entry, class, "display", value.into());
apply!(cx, classes, self.direction, "_flex-direction_", "flex-direction");
apply!(cx, classes, self.wrap, "_flex-wrap_", "flex-wrap");
apply!(cx, classes, self.justify, "_flex-justify_", "justify-content");
apply!(cx, classes, self.align, "_flex-align-items_", "align-items");
apply!(cx, classes, self.align_content, "_flex-align-content_", "align-content");
for (bp, gap) in self.gap.by_breakpoint() {
let entry = bp.resolved(cx);
for (property, value) in gap.styles().into_iter().flatten() {
// El prefijo de `gap` no es literal (depende de la propiedad), así que se compone
// aquí en un único `join!` en vez de pasar por `responsive_class!`.
let token = value_to_token(&value);
let class = match entry {
None => util::join!("_flex-", property, "_", token, "_"),
Some(e) => util::join!("_flex-", property, "_", token, "_", e.name, "_"),
};
styles(cx, classes, entry, class.into(), property, value);
}
}
}
}

View file

@ -1,26 +1,36 @@
use crate::core::component::Context;
use crate::core::theme::{Breakpoint, Responsive};
use crate::html::PropsOp;
use crate::html::flex::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
use crate::html::align;
use crate::html::flex::{ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
use crate::{AutoDefault, Getters, builder_impl, util};
/// Configuración de un elemento como ítem de un contenedor Flexbox.
///
/// A diferencia de [`Flex`](crate::html::flex::Flex), que configura el comportamiento Flexbox
/// global de un contenedor y sus hijos como grupo, `FlexItem` configura un único elemento en
/// relación con el contenedor flex padre: crecimiento ([`ItemGrow`]), reducción ([`ItemShrink`]),
/// alineación individual ([`ItemAlign`]), orden visual ([`ItemOrder`]), tamaño ([`ItemSize`]) y
/// desplazamiento ([`ItemOffset`]).
/// A diferencia del componente [`Flex`], que define un contenedor que aplica Flexbox para el
/// posicionamiento de sus componentes hijo, `FlexItem` se aplica sobre un único elemento en
/// relación con el contenedor flex padre. Usa el método `with_prop()` que suele exponer cualquier
/// componente, y acepta `FlexItem` directamente gracias a su `From` hacia [`PropsOp`].
///
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
/// componente puede acabar siendo hijo de un contenedor flex, y ninguno debería necesitar un campo
/// propio para esto. Se aplica sobre el `with_prop()` que suele exponer cualquier componente, que
/// acepta `FlexItem` directamente gracias a su `From` hacia [`PropsOp`].
/// propio para esto.
///
/// `FlexItem` actúa sobre propiedades del elemento para configurar su crecimiento ([`ItemGrow`]),
/// reducción ([`ItemShrink`]), alineación individual ([`align::ItemSelf`]), orden visual
/// ([`ItemOrder`]), tamaño ([`ItemSize`]) y desplazamiento ([`ItemOffset`]).
///
/// Un hijo de `Flex` sin ningún `FlexItem` aplicado participa igualmente como ítem flex, sólo que
/// sin crecimiento, reducción, alineación individual, orden, tamaño ni desplazamiento propios
/// (todos sus valores por defecto).
///
/// Con [`ItemSize`] y [`ItemOffset`] se pueden modelar rejillas de columnas fijas sobre Flexbox,
/// combinando un tamaño en fracción del contenedor con un desplazamiento lateral cuando se
/// necesite.
///
/// [`Flex`]: crate::base::component::Flex
/// [`align::ItemSelf`]: crate::html::align::ItemSelf
///
/// # Ejemplo
///
/// ```rust,no_run
@ -50,7 +60,7 @@ pub struct FlexItem {
shrink: Responsive<ItemShrink>,
/// Devuelve la alineación individual en el eje transversal, por punto de corte.
#[getters(copy)]
align_self: Responsive<ItemAlign>,
align_self: Responsive<align::ItemSelf>,
/// Devuelve la posición en el orden visual, por punto de corte.
#[getters(copy)]
order: Responsive<ItemOrder>,
@ -124,14 +134,14 @@ impl FlexItem {
}
/// Establece la alineación individual en el eje transversal.
pub fn with_align_self(mut self, align_self: ItemAlign) -> Self {
pub fn with_align_self(mut self, align_self: align::ItemSelf) -> Self {
self.align_self = self.align_self.set(align_self);
self
}
/// Establece la alineación individual en el eje transversal, a partir del punto de corte
/// indicado.
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: ItemAlign) -> Self {
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: align::ItemSelf) -> Self {
self.align_self = self.align_self.set_at(bp, align_self);
self
}
@ -152,7 +162,7 @@ impl FlexItem {
/// [`ItemShrink::Is0`](super::ItemShrink::Is0) por sí solo (consulta la documentación de
/// [`ItemSize`] antes de combinarlo con [`with_shrink()`](Self::with_shrink) porque con un
/// tamaño en porcentaje, forzar `ItemShrink::Is0` sólo es seguro si el contenedor no tiene
/// [`Gap`](super::Gap)).
/// [`align::Gap`](crate::html::align::Gap)).
pub fn with_size(mut self, size: ItemSize) -> Self {
self.size = self.size.set(size);
self
@ -189,10 +199,12 @@ impl FlexItem {
/// que sólo establezca `with_size_at(Breakpoint::Lg, ...)` no borra el `with_size()` base que
/// `self` ya tuviera, sólo sustituye la entrada de ese punto de corte.
///
/// Es el método que usa [`Props::with_prop()`](crate::html::props::Props::with_prop) para que
/// sucesivas [`PropsOp::FlexItem`](crate::html::props::PropsOp::FlexItem) sobre el mismo
/// componente vayan completando campos concretos sin repetir los ya establecidos, en vez de
/// partir de cero en cada llamada.
/// Es el método que usa [`Props::with_prop()`] para que sucesivas [`PropsOp::FlexItem`] sobre
/// el mismo componente vayan completando campos concretos sin repetir los ya establecidos, en
/// vez de partir de cero en cada llamada.
///
/// [`Props::with_prop()`]: crate::html::props::Props::with_prop
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
pub fn merge(mut self, item: FlexItem) -> Self {
self.grow = self.grow.merge(item.grow);
self.shrink = self.shrink.merge(item.shrink);
@ -204,14 +216,13 @@ impl FlexItem {
}
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
/// [`Flex::apply()`](super::Flex::apply): cada faceta con valor añade una declaración de
/// estilo (por punto de corte, si se ha establecido alguno) y su propia clase. Un campo sin
/// ningún valor establecido, o con un valor cuya variante es la "por defecto" del propio enum
/// (p. ej. `ItemGrow::Default`), no añade nada.
/// hace internamente [`Flex`](crate::base::component::Flex): cada propiedad con valor añade una
/// declaración de estilo (por punto de corte, si se ha establecido alguno) y su propia clase.
/// Un campo sin ningún valor establecido, o con un valor cuya variante es la "por defecto" del
/// propio enum (p. ej. `ItemGrow::Default`), no añade nada.
///
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
/// para poder compartir un único acumulador con [`Flex::apply()`](super::Flex::apply) sin
/// cadenas intermedias.
/// para poder compartir un único acumulador con las de `Flex` sin cadenas intermedias.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};

View file

@ -1,98 +1,17 @@
//! Enums semánticos que configuran [`Flex`](super::Flex), a nivel de contenedor.
use crate::html::unit::UnitValue;
use crate::{AutoDefault, CowStr};
// **< Align >**************************************************************************************
/// Alineación de los elementos en el eje transversal de un contenedor [`Flex`](super::Flex).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Align {
/// Por defecto (`align-items: normal` no explícito), mismo efecto que [`Align::Stretch`], salvo
/// que el elemento tenga su propio tamaño.
#[default]
Default,
/// Alinea los elementos al inicio del eje transversal (`align-items: flex-start`).
Start,
/// Alinea los elementos al final del eje transversal (`align-items: flex-end`).
End,
/// Centra los elementos en el eje transversal (`align-items: center`).
Center,
/// Alinea los elementos por su línea base de texto (`align-items: baseline`).
Baseline,
/// Estira los elementos para ocupar todo el eje transversal (`align-items: stretch`).
Stretch,
}
impl Align {
// Devuelve el valor CSS de `align-items`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Start => "flex-start".into(),
Self::End => "flex-end".into(),
Self::Center => "center".into(),
Self::Baseline => "baseline".into(),
Self::Stretch => "stretch".into(),
}
}
}
// **< AlignContent >*******************************************************************************
/// Alineación de varias líneas en un contenedor [`Flex`](super::Flex).
///
/// Sólo tiene efecto si el contenedor usa [`Behavior::Wrap`] o [`Behavior::WrapReverse`] y genera
/// más de una línea de elementos.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum AlignContent {
/// Por defecto (`align-content: normal` no explícito), como en [`AlignContent::Stretch`], las
/// líneas se estiran para ocupar el espacio sobrante del eje transversal, sin efecto visible si
/// el contenedor no tiene ningún espacio sobrante que repartir (p. ej. una altura `auto`
/// ajustada al contenido).
#[default]
Default,
/// Alinea las líneas al inicio del eje transversal (`align-content: flex-start`).
Start,
/// Alinea las líneas al final del eje transversal (`align-content: flex-end`).
End,
/// Centra las líneas en el eje transversal (`align-content: center`).
Center,
/// Reparte el espacio sobrante entre las líneas (`align-content: space-between`).
SpaceBetween,
/// Reparte el espacio sobrante alrededor de cada línea (`align-content: space-around`).
SpaceAround,
/// Reparte el espacio sobrante en partes iguales, incluidos los extremos
/// (`align-content: space-evenly`).
SpaceEvenly,
/// Estira las líneas para ocupar todo el eje transversal (`align-content: stretch`).
Stretch,
}
impl AlignContent {
// Devuelve el valor CSS de `align-content`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Start => "flex-start".into(),
Self::End => "flex-end".into(),
Self::Center => "center".into(),
Self::SpaceBetween => "space-between".into(),
Self::SpaceAround => "space-around".into(),
Self::SpaceEvenly => "space-evenly".into(),
Self::Stretch => "stretch".into(),
}
}
}
// **< Behavior >***********************************************************************************
/// Comportamiento de los elementos si no caben en una línea del contenedor [`Flex`](super::Flex).
/// Comportamiento de los elementos si no caben en una línea del contenedor [`Flex`].
///
/// Si el contenedor aplica [`Gap`] y un [`ItemSize`](super::ItemSize) porcentual en los hijos,
/// entonces usar [`Behavior::Wrap`] en vez de [`Behavior::NoWrap`] (su valor por defecto) puede
/// provocar saltos de línea prematuros. En la sección "Cómo combinarlo con `Gap`" de `ItemSize`
/// se explica el porqué.
/// Si el contenedor aplica [`align::Gap`] y un [`ItemSize`] porcentual en los hijos, entonces usar
/// [`Behavior::Wrap`] en vez de [`Behavior::NoWrap`] (su valor por defecto) puede provocar saltos
/// de línea prematuros. En la sección "Cómo combinarlo con `Gap`" de `ItemSize` se explica el
/// porqué.
///
/// [`Flex`]: crate::base::component::Flex
/// [`align::Gap`]: crate::html::align::Gap
/// [`ItemSize`]: crate::html::flex::ItemSize
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Behavior {
/// Por defecto, no se dividen en varias líneas: se comprimen o desbordan (`flex-wrap: nowrap`
@ -108,7 +27,7 @@ pub enum Behavior {
impl Behavior {
// Devuelve el valor CSS de `flex-wrap`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
pub(crate) fn value(self) -> CowStr {
match self {
Self::NoWrap => "".into(),
Self::Wrap => "wrap".into(),
@ -119,13 +38,14 @@ impl Behavior {
// **< ContentJustify >*****************************************************************************
/// Alineación de los elementos en el eje principal de un contenedor [`Flex`](super::Flex).
/// Alineación de los elementos en el eje principal de un contenedor
/// [`Flex`](crate::base::component::Flex).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ContentJustify {
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-content: normal` no
/// explícito).
#[default]
Default,
Normal,
/// Alinea los elementos al inicio del eje principal (`justify-content: flex-start`).
Start,
/// Alinea los elementos al final del eje principal (`justify-content: flex-end`).
@ -143,9 +63,9 @@ pub enum ContentJustify {
impl ContentJustify {
// Devuelve el valor CSS de `justify-content`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
pub(crate) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Normal => "".into(),
Self::Start => "flex-start".into(),
Self::End => "flex-end".into(),
Self::Center => "center".into(),
@ -158,7 +78,7 @@ impl ContentJustify {
// **< Direction >**********************************************************************************
/// Dirección del eje principal de un contenedor [`Flex`](super::Flex).
/// Dirección del eje principal de un contenedor [`Flex`](crate::base::component::Flex).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Direction {
/// Por defecto, los elementos se disponen en fila, de izquierda a derecha
@ -175,7 +95,7 @@ pub enum Direction {
impl Direction {
// Devuelve el valor CSS de `flex-direction`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
pub(crate) fn value(self) -> CowStr {
match self {
Self::Row => "".into(),
Self::RowReverse => "row-reverse".into(),
@ -184,44 +104,3 @@ impl Direction {
}
}
}
// **< Gap >****************************************************************************************
/// Espaciado entre los elementos de un contenedor [`Flex`](super::Flex).
///
/// Es un valor continuo, no una utilidad predefinida: se resuelve siempre como estilo
/// `gap`/`row-gap`/`column-gap` en línea, igual que el resto de facetas de
/// [`Flex`](super::Flex)/[`FlexItem`](super::FlexItem).
///
/// Si se combina con un [`ItemSize`](super::ItemSize) porcentual sobre los hijos, la sección "Cómo
/// combinarlo con `Gap`" de `ItemSize` explica cómo evitar que el hueco desborde el contenedor.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Gap {
/// Por defecto, no hay espaciado (`gap: normal` no explícito).
#[default]
None,
/// Mismo espaciado entre filas y columnas.
Both(UnitValue),
/// Espaciado distinto entre filas y columnas.
Distinct { row: UnitValue, column: UnitValue },
}
impl Gap {
// Declaraciones de estilo (propiedad, valor) para este espaciado; cada hueco a `None` si no hay
// ninguna medible (`UnitValue::None`/`UnitValue::Auto` no producen ningún estilo).
pub(super) fn styles(self) -> [Option<(&'static str, CowStr)>; 2] {
match self {
Self::None => [None, None],
Self::Both(value) => [Self::style("gap", value), None],
Self::Distinct { row, column } => [
Self::style("row-gap", row),
Self::style("column-gap", column),
],
}
}
// Declaración (propiedad, valor) para un valor medible, o `None` si no lo es.
fn style(property: &'static str, value: UnitValue) -> Option<(&'static str, CowStr)> {
value.is_measurable().then(|| (property, value.into()))
}
}

View file

@ -1,42 +1,6 @@
//! Enums semánticos que configuran [`FlexItem`](super::FlexItem), a nivel de ítem.
use crate::html::unit::UnitValue;
use crate::{AutoDefault, CowStr};
// **< ItemAlign >**********************************************************************************
/// Alineación en [`FlexItem`](super::FlexItem) para un ítem, sobrescribiendo la del contenedor.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemAlign {
/// Por defecto, hereda la alineación del contenedor (`align-self: auto` no explícito).
#[default]
Default,
/// Alinea el ítem al inicio del eje transversal (`align-self: flex-start`).
Start,
/// Alinea el ítem al final del eje transversal (`align-self: flex-end`).
End,
/// Centra el ítem en el eje transversal (`align-self: center`).
Center,
/// Alinea el ítem por su línea base de texto (`align-self: baseline`).
Baseline,
/// Estira el ítem para ocupar todo el eje transversal (`align-self: stretch`).
Stretch,
}
impl ItemAlign {
// Devuelve el valor CSS de `align-self`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Start => "flex-start".into(),
Self::End => "flex-end".into(),
Self::Center => "center".into(),
Self::Baseline => "baseline".into(),
Self::Stretch => "stretch".into(),
}
}
}
// **< ItemGrow >***********************************************************************************
/// Factor de crecimiento en [`FlexItem`](super::FlexItem) para un ítem dentro de un contenedor.
@ -197,10 +161,12 @@ impl ItemOrder {
/// elemento el que ceda (uno con [`ItemGrow::Is1`] y contenido que sí admita reajuste, como texto),
/// no éste.
///
/// Con [`ItemSize`] en porcentaje, `Is0` es seguro si el contenedor no tiene [`Gap`](super::Gap)
/// (sin `gap` no hay nada que compensar). Pero **si el contenedor tiene `Gap`, no combines `Is0`
/// con un tamaño porcentual** porque desactivas la única pieza (el reparto del espacio negativo
/// entre elementos) que compensa el hueco por ti. La explicación completa está en [`ItemSize`].
/// Con [`ItemSize`] en porcentaje, `Is0` es seguro si el contenedor no tiene [`align::Gap`] (sin
/// `gap` no hay nada que compensar). Pero **si el contenedor tiene `Gap`, no combines `Is0` con un
/// tamaño porcentual** porque desactivas la única pieza (el reparto del espacio negativo entre
/// elementos) que compensa el hueco por ti. La explicación completa está en [`ItemSize`].
///
/// [`align::Gap`]: crate::html::align::Gap
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemShrink {
/// Por defecto, puede encoger si hace falta (`flex-shrink: 1` no explícito).
@ -234,10 +200,10 @@ impl ItemShrink {
/// # Cómo combinarlo con `Gap`
///
/// Un porcentaje se resuelve contra el ancho del contenedor sin contar el espacio que va a ocupar
/// el [`Gap`](super::Gap). Es una limitación del propio CSS, porque `flex-basis` en porcentaje usa
/// la misma regla de resolución que cualquier `width: %`. Si los porcentajes de una fila suman el
/// 100% (una rejilla completa, el caso habitual), el hueco que añade `gap` sobra respecto al ancho
/// del contenedor.
/// el [`align::Gap`](crate::html::align::Gap). Es una limitación del propio CSS, porque
/// `flex-basis` en porcentaje usa la misma regla de resolución que cualquier `width: %`. Si los
/// porcentajes de una fila suman el 100% (una rejilla completa, el caso habitual), el hueco que
/// añade `gap` sobra respecto al ancho del contenedor.
///
/// Ese sobrante se compensa solo, sin ningún ajuste manual, siempre que:
///

54
src/html/grid.rs Normal file
View file

@ -0,0 +1,54 @@
//! Definiciones para el posicionamiento de componentes con [CSS Grid].
//!
//! Al igual que ocurre con [`Flex`]/[`FlexItem`], por un lado está [`Grid`], el **componente
//! contenedor** de [`pagetop::base::component`], que define una rejilla de filas y columnas sobre
//! la que se aplican pistas, alineación y espaciado, con sus propios constructores (`new()`,
//! `at()`, `inline()`, `inline_at()`) y builders (`with_columns()`, `with_gap()`, etc.).
//!
//! Y por otro, aquí se define [`GridItem`], que es la **configuración** que se aplica sobre un
//! único elemento dentro de la rejilla de su padre (columna, fila, alineación individual). No tiene
//! un builder propio ya que podría usarse sobre cualquier componente. Por eso se aplica pasándolo
//! directamente al `with_prop()` que normalmente ya expone cualquier componente, gracias a su
//! implementación `From` hacia [`PropsOp`].
//!
//! # Un entorno nativo autosuficiente
//!
//! `Grid`/`GridItem` reutilizan el mismo esquema que [`Flex`]/[`FlexItem`] para generar clases CSS
//! dinámicamente, independientes de cualquier tema o framework CSS, registradas vía
//! [`AssetsOp::add_responsive_style()`] y renderizadas en el `<head>` del documento. El nombre 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 generada en lugar de duplicarla.
//!
//! # Qué se comparte con Flex, y qué no
//!
//! Ambos sistemas comparten varias propiedades con el mismo catálogo de valores, `align-items`,
//! `align-content`, `align-self`, `gap`, disponibles en [`pagetop::html::align`].
//!
//! No se comparte nada para `justify-items`/`justify-self` porque no hay equivalencia en Flexbox
//! (sólo tiene un eje principal y uno transversal, intercambiables con [`Direction`], no dos ejes
//! independientes como Grid) ni para `justify-content` ya que aunque el nombre de la propiedad
//! coincide con el de Flex, Grid admite `stretch` (un valor sin efecto en Flexbox que
//! [`flex::ContentJustify`] no modela), así que [`grid::ContentJustify`] es un catálogo propio de
//! este módulo, no compartido.
//!
//! [CSS Grid]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
//! [`pagetop::html::align`]: crate::html::align
//! [`FlexItem`]: crate::html::flex::FlexItem
//! [`flex::ContentJustify`]: crate::html::flex::ContentJustify
//! [`grid::ContentJustify`]: crate::html::grid::ContentJustify
//! [`Direction`]: crate::html::flex::Direction
//! [`PropsOp`]: crate::html::props::PropsOp
//! [`pagetop::base::component`]: crate::base::component
//! [`Flex`]: crate::base::component::Flex
//! [`Grid`]: crate::base::component::Grid
mod props_container;
pub use props_container::MAX_TRACKS;
pub use props_container::{AutoFlow, AxisTrack, ContentJustify, DefaultJustify, Tracks};
mod props_item;
pub use props_item::{ItemJustify, ItemPlacement};
mod item;
pub use item::GridItem;

166
src/html/grid/item.rs Normal file
View file

@ -0,0 +1,166 @@
use crate::core::component::Context;
use crate::core::theme::{Breakpoint, Responsive};
use crate::html::PropsOp;
use crate::html::align;
use crate::html::grid::{ItemJustify, ItemPlacement};
use crate::{AutoDefault, Getters, builder_impl, util};
/// Configuración de un elemento como ítem de un contenedor CSS Grid.
///
/// A diferencia del componente [`Grid`], que define un contenedor que aplica CSS Grid para el
/// posicionamiento de sus componentes hijo, `GridItem` se aplica sobre un único elemento en
/// relación con la rejilla del contenedor padre. Usa el método `with_prop()` que suele exponer
/// cualquier componente, y acepta `GridItem` directamente gracias a su `From` hacia [`PropsOp`].
///
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
/// componente puede acabar siendo hijo de un contenedor Grid, y ninguno debería necesitar un campo
/// propio para esto.
///
/// `GridItem` actúa sobre propiedades del elemento para configurar su posición en columna y en
/// fila ([`ItemPlacement`]), alineación individual en el eje de columnas ([`ItemJustify`]) y en el
/// eje de filas ([`align::ItemSelf`]).
///
/// Un hijo de `Grid` sin ningún `GridItem` aplicado participa igualmente en la colocación
/// automática, sólo que sin ninguna posición ni alineación propia.
///
/// [`Grid`]: crate::base::component::Grid
/// [`align::ItemSelf`]: crate::html::align::ItemSelf
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// // Ocupa las tres primeras columnas de la rejilla de su `Grid` padre.
/// let banner = Container::new()
/// .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(3)));
///
/// // Empieza en la columna 2 y termina antes de la 4, alineado al final de su fila.
/// let sidebar = Container::new().with_prop(
/// GridItem::new()
/// .with_column(grid::ItemPlacement::Range(2, 4))
/// .with_align_self(align::ItemSelf::End),
/// );
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct GridItem {
/// Devuelve la posición en columna, por punto de corte.
#[getters(copy)]
column: Responsive<ItemPlacement>,
/// Devuelve la posición en fila, por punto de corte.
#[getters(copy)]
row: Responsive<ItemPlacement>,
/// Devuelve la alineación individual en el eje de columnas, por punto de corte.
#[getters(copy)]
justify_self: Responsive<ItemJustify>,
/// Devuelve la alineación individual en el eje de filas, por punto de corte.
#[getters(copy)]
align_self: Responsive<align::ItemSelf>,
}
#[builder_impl]
impl GridItem {
/// Crea una configuración de ítem con todos los valores por defecto.
pub fn new() -> Self {
Self::default()
}
// **< GridItem BUILDER >***********************************************************************
/// Establece la posición en columna.
pub fn with_column(mut self, placement: ItemPlacement) -> Self {
self.column = self.column.set(placement);
self
}
/// Establece la posición en columna, a partir del punto de corte indicado.
pub fn with_column_at(mut self, bp: Breakpoint, placement: ItemPlacement) -> Self {
self.column = self.column.set_at(bp, placement);
self
}
/// Establece la posición en fila.
pub fn with_row(mut self, placement: ItemPlacement) -> Self {
self.row = self.row.set(placement);
self
}
/// Establece la posición en fila, a partir del punto de corte indicado.
pub fn with_row_at(mut self, bp: Breakpoint, placement: ItemPlacement) -> Self {
self.row = self.row.set_at(bp, placement);
self
}
/// Establece la alineación individual en el eje de columnas.
pub fn with_justify_self(mut self, justify_self: ItemJustify) -> Self {
self.justify_self = self.justify_self.set(justify_self);
self
}
/// Establece la alineación individual en el eje de columnas, a partir del punto de corte
/// indicado.
pub fn with_justify_self_at(mut self, bp: Breakpoint, justify_self: ItemJustify) -> Self {
self.justify_self = self.justify_self.set_at(bp, justify_self);
self
}
/// Establece la alineación individual en el eje de filas.
pub fn with_align_self(mut self, align_self: align::ItemSelf) -> Self {
self.align_self = self.align_self.set(align_self);
self
}
/// Establece la alineación individual en el eje de filas, a partir del punto de corte
/// indicado.
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: align::ItemSelf) -> Self {
self.align_self = self.align_self.set_at(bp, align_self);
self
}
}
impl GridItem {
/// Combina esta configuración con otra `GridItem`, campo a campo.
///
/// La fusión llega al nivel de cada punto de corte; donde `item` tenga un valor establecido,
/// sustituye al de `self` y donde no lo tenga, se conserva el que ya hubiera. Por eso un `item`
/// que sólo establezca `with_column_at(Breakpoint::Lg, ...)` no borra el `with_column()` base
/// que `self` ya tuviera, sólo sustituye la entrada de ese punto de corte.
///
/// Es el método que usa [`Props::with_prop()`] para que sucesivas [`PropsOp::GridItem`] sobre
/// el mismo componente vayan completando campos concretos sin repetir los ya establecidos, en
/// vez de partir de cero en cada llamada.
///
/// [`Props::with_prop()`]: crate::html::props::Props::with_prop
/// [`PropsOp::GridItem`]: crate::html::props::PropsOp::GridItem
pub fn merge(mut self, item: GridItem) -> Self {
self.column = self.column.merge(item.column);
self.row = self.row.merge(item.row);
self.justify_self = self.justify_self.merge(item.justify_self);
self.align_self = self.align_self.merge(item.align_self);
self
}
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
/// hace internamente [`Grid`](crate::base::component::Grid): cada propiedad con valor añade una
/// declaración de estilo (por punto de corte, si se ha establecido alguno) y su propia clase.
/// Un campo sin ningún valor establecido, o con un valor cuya variante es la "por defecto" del
/// propio enum (p. ej. `ItemJustify::Default`), no añade nada.
///
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
/// para poder compartir un único acumulador con las de `Grid` sin cadenas intermedias.
#[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.column, "_grid-item-column_", "grid-column", val);
apply!(cx, classes, self.row, "_grid-item-row_", "grid-row", val);
apply!(cx, classes, self.justify_self, "_grid-item-justify-self_", "justify-self");
apply!(cx, classes, self.align_self, "_grid-item-align-self_", "align-self");
}
}
impl From<GridItem> for PropsOp {
fn from(item: GridItem) -> Self {
Self::grid_item(item)
}
}

View file

@ -0,0 +1,263 @@
use crate::html::unit::UnitValue;
use crate::{AutoDefault, CowStr, util};
// **< AutoFlow >***********************************************************************************
/// Mecanismo de colocación automática de los elementos en un contenedor [`Grid`].
///
/// Aplica en aquellos elementos que no tienen una posición explícita ([`ItemPlacement`]) en el
/// contenedor.
///
/// [`Grid`]: crate::base::component::Grid
/// [`ItemPlacement`]: super::ItemPlacement
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum AutoFlow {
/// Por defecto, coloca automáticamente rellenando filas (`grid-auto-flow: row` no explícito).
#[default]
Row,
/// Coloca automáticamente rellenando columnas (`grid-auto-flow: column`).
Column,
/// Como `Row`, pero rellena huecos anteriores si un elemento más pequeño cabe en ellos
/// (`grid-auto-flow: row dense`). Puede alterar el orden visual respecto al del documento con
/// la misma cautela de accesibilidad que ya documenta [`ItemOrder`] en Flex.
///
/// [`ItemOrder`]: crate::html::flex::ItemOrder
RowDense,
/// Como `Column`, con el mismo relleno de huecos (`grid-auto-flow: column dense`).
ColumnDense,
}
impl AutoFlow {
// Devuelve el valor CSS de `grid-auto-flow`, o "" para el valor por defecto.
pub(crate) fn value(self) -> CowStr {
match self {
Self::Row => "".into(),
Self::Column => "column".into(),
Self::RowDense => "row dense".into(),
Self::ColumnDense => "column dense".into(),
}
}
}
// **< DefaultJustify >*****************************************************************************
/// Alineación de los elementos en el eje de columnas (en línea) de un contenedor [`Grid`].
///
/// Es el valor por defecto que hereda cualquier elemento siempre que [`ItemJustify`] no lo
/// sobrescriba.
///
/// Es al eje de columnas lo que [`align::Items`] es al eje transversal de [`Flex`], pero Grid, a
/// diferencia de Flexbox, sí tiene una propiedad CSS propia para ello (`justify-items`), porque en
/// Grid ningún eje se resuelve "girando" al otro.
///
/// [`align::Items`]: crate::html::align::Items
/// [`Flex`]: crate::base::component::Flex
/// [`Grid`]: crate::base::component::Grid
/// [`ItemJustify`]: super::ItemJustify
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum DefaultJustify {
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-items: normal` no
/// explícito, equivalente a `stretch` salvo que el elemento tenga su propio tamaño).
#[default]
Normal,
/// Alinea los elementos al inicio del eje de columnas (`justify-items: start`).
Start,
/// Alinea los elementos al final del eje de columnas (`justify-items: end`).
End,
/// Centra los elementos en el eje de columnas (`justify-items: center`).
Center,
/// Estira los elementos para ocupar toda su columna (`justify-items: stretch`).
Stretch,
}
impl DefaultJustify {
// Devuelve el valor CSS de `justify-items`, o "" para el valor por defecto.
pub(crate) fn value(self) -> CowStr {
match self {
Self::Normal => "".into(),
Self::Start => "start".into(),
Self::End => "end".into(),
Self::Center => "center".into(),
Self::Stretch => "stretch".into(),
}
}
}
// **< ContentJustify >*****************************************************************************
/// Alineación de las pistas en el eje de columnas de un contenedor [`Grid`].
///
/// Aplica cuando su tamaño total es menor que el del propio contenedor. No se reutiliza
/// [`flex::ContentJustify`] porque Grid admite `stretch` (estira las pistas para repartir el
/// espacio sobrante entre ellas), un valor sin efecto en Flexbox.
///
/// [`Grid`]: crate::base::component::Grid
/// [`flex::ContentJustify`]: crate::html::flex::ContentJustify
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ContentJustify {
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-content: normal` no
/// explícito).
#[default]
Normal,
/// Alinea las pistas al inicio del eje de columnas (`justify-content: start`).
Start,
/// Alinea las pistas al final del eje de columnas (`justify-content: end`).
End,
/// Centra las pistas en el eje de columnas (`justify-content: center`).
Center,
/// Estira las pistas para repartir el espacio sobrante entre ellas
/// (`justify-content: stretch`).
Stretch,
/// Reparte el espacio sobrante entre las pistas (`justify-content: space-between`).
SpaceBetween,
/// Reparte el espacio sobrante alrededor de cada pista (`justify-content: space-around`).
SpaceAround,
/// Reparte el espacio sobrante en partes iguales, incluidos los extremos
/// (`justify-content: space-evenly`).
SpaceEvenly,
}
impl ContentJustify {
// Devuelve el valor CSS de `justify-content`, o "" para el valor por defecto.
pub(crate) fn value(self) -> CowStr {
match self {
Self::Normal => "".into(),
Self::Start => "start".into(),
Self::End => "end".into(),
Self::Center => "center".into(),
Self::Stretch => "stretch".into(),
Self::SpaceBetween => "space-between".into(),
Self::SpaceAround => "space-around".into(),
Self::SpaceEvenly => "space-evenly".into(),
}
}
}
// **< AxisTrack >**********************************************************************************
/// Anchura o altura de una única pista en un contenedor [`Grid`].
///
/// Se aplica dentro de una lista [`Tracks`], o vía [`Grid::with_columns()`]/[`Grid::with_rows()`]
/// (la rejilla explícita); o suelto, vía [`Grid::with_auto_columns()`]/[`Grid::with_auto_rows()`]
/// (el tamaño de las pistas que el navegador genera automáticamente fuera de esa rejilla).
///
/// [`Grid`]: crate::base::component::Grid
/// [`Grid::with_columns()`]: crate::base::component::Grid::with_columns
/// [`Grid::with_rows()`]: crate::base::component::Grid::with_rows
/// [`Grid::with_auto_columns()`]: crate::base::component::Grid::with_auto_columns
/// [`Grid::with_auto_rows()`]: crate::base::component::Grid::with_auto_rows
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum AxisTrack {
/// Por defecto, ajusta al contenido repartiendo el espacio sobrante con las demás pistas `Auto`
/// (`auto`).
#[default]
Auto,
/// Medida fija o porcentual (p. ej. `AxisTrack::Fixed(UnitValue::Px(200))` o
/// `AxisTrack::Fixed(UnitValue::RelPct(50.0))`).
Fixed(UnitValue),
/// Fracción del espacio sobrante repartido entre todas las pistas `Fraction` del mismo
/// contenedor (`<n>fr`). Es la unidad idiomática de Grid, sin equivalente en [`UnitValue`]
/// porque no es una medida absoluta ni relativa a nada fuera del propio reparto de Grid.
Fraction(f32),
/// El tamaño más pequeño en el que el contenido cabe sin desbordar. El texto se ajusta de línea
/// todo lo posible, así que el resultado es el ancho de su elemento indivisible más largo
/// (`min-content`).
MinContent,
/// El tamaño que ocuparía el contenido si no tuviera que ajustarse de línea en absoluto, siendo
/// su ancho natural, sin envolver (`max-content`).
MaxContent,
}
impl AxisTrack {
// Devuelve el valor CSS de esta pista. A diferencia del resto de propiedades de Grid/GridItem,
// un `AxisTrack::Auto` (el valor por defecto del enum) sí produce un valor no vacío ("auto").
// En vez de omitir la propiedad entera, una lista de pistas necesita un valor por cada hueco
// para que el número de pistas declaradas sea el correcto.
pub(crate) fn value(self) -> CowStr {
match self {
Self::Auto => "auto".into(),
Self::Fixed(unit) => unit.value(),
Self::Fraction(n) => util::join!(n.to_string(), "fr").into(),
Self::MinContent => "min-content".into(),
Self::MaxContent => "max-content".into(),
}
}
}
// **< Tracks >*************************************************************************************
/// Número máximo de pistas que admite [`Tracks`].
///
/// Ver la nota de la propia `Tracks` sobre por qué es una capacidad fija y no un `Vec`.
pub const MAX_TRACKS: usize = 12;
/// Lista ordenada de pistas para la rejilla explícita de un contenedor [`Grid`].
///
/// Sólo se aplica para [`Grid::with_columns()`] o [`Grid::with_rows()`].
///
/// [`Grid`]: crate::base::component::Grid
/// [`Grid::with_columns()`]: crate::base::component::Grid::with_columns
/// [`Grid::with_rows()`]: crate::base::component::Grid::with_rows
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Tracks {
// Es un array de capacidad fija (`[Option<AxisTrack>; MAX_TRACKS]`), no un `Vec<AxisTrack>`,
// porque [`Responsive<T>`](crate::core::theme::Responsive) exige `T: Copy` y un `Vec` no lo es;
// de lo contrario `Tracks` no podría variar por punto de corte (p. ej. una columna en móvil,
// tres en escritorio), que es precisamente el caso de uso más habitual de Grid con breakpoints.
// Doce pistas es más que suficiente para cualquier rejilla real (coincide, no por casualidad,
// con la tradición de 12 columnas de Bootstrap y otros frameworks CSS).
tracks: [Option<AxisTrack>; MAX_TRACKS],
}
impl Tracks {
/// Crea una lista de pistas vacía.
pub fn new() -> Self {
Self::default()
}
/// Añade una pista al final de la lista. Si ya hay [`MAX_TRACKS`] pistas, la llamada se ignora.
pub fn with_track(mut self, track: AxisTrack) -> Self {
if let Some(slot) = self.tracks.iter_mut().find(|t| t.is_none()) {
*slot = Some(track);
}
self
}
/// Repite la misma pista `count` veces (equivalente a `repeat(count, track)` en CSS, para el
/// caso de uso más común con pistas idénticas). Trunca a [`MAX_TRACKS`] si `count` lo supera.
///
/// ```rust
/// # use pagetop::prelude::*;
/// let tracks = grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0));
/// assert_eq!(tracks.to_string(), "1fr 1fr 1fr");
/// ```
pub fn repeat(count: usize, track: AxisTrack) -> Self {
let mut tracks = Self::new();
for _ in 0..count.min(MAX_TRACKS) {
tracks = tracks.with_track(track);
}
tracks
}
// Serializa como valor CSS. Las pistas separadas por un espacio, en orden, ignorando los huecos
// vacíos. Cadena vacía si no hay ninguna pista (para que `apply!` no genere ninguna clase,
// igual que el resto de propiedades con valor "por defecto" de Flex).
pub(crate) fn value(self) -> CowStr {
self.tracks
.iter()
.flatten()
.map(|t| t.value())
.collect::<Vec<_>>()
.join(" ")
.into()
}
}
// Único acceso público al valor CSS de `pub(crate) value()`, invisible fuera del crate, incluidos
// los propios doctests, que compilan como si fueran un crate externo (ver el ejemplo de `repeat()`,
// que depende de esta implementación para poder comprobar el resultado).
impl std::fmt::Display for Tracks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value())
}
}

View file

@ -0,0 +1,83 @@
use crate::{AutoDefault, CowStr, util};
// **< ItemPlacement >******************************************************************************
/// Posición de un [`GridItem`](super::GridItem) en una línea de la rejilla (columna o fila).
///
/// El mismo tipo sirve para `grid-column` y `grid-row` ([`GridItem`](super::GridItem) guarda una
/// instancia independiente para cada eje).
///
/// Las líneas se numeran desde `1` (la primera línea antes de la primera pista); un número negativo
/// cuenta desde el final de la rejilla explícita (`-1` es la última línea). El `0` no es una línea
/// válida en CSS Grid y se trata como [`ItemPlacement::Auto`].
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemPlacement {
/// Colocación automática, según `grid-auto-flow` del contenedor (valor por defecto, sin
/// declaración explícita).
#[default]
Auto,
/// Ocupa `n` pistas a partir de donde lo coloque el algoritmo de colocación automática
/// (`span n`).
Span(u16),
/// Empieza en la línea indicada, ocupando una sola pista.
Line(i16),
/// Ocupa desde la primera línea hasta la segunda, ambas indicadas explícitamente
/// (`<start> / <end>`).
Range(i16, i16),
}
impl ItemPlacement {
// Devuelve el valor CSS de `grid-column`/`grid-row`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Auto => "".into(),
Self::Span(0) => "".into(),
Self::Span(n) => util::join!("span ", n.to_string()).into(),
Self::Line(0) => "".into(),
Self::Line(n) => n.to_string().into(),
Self::Range(0, _) | Self::Range(_, 0) => "".into(),
Self::Range(start, end) => {
util::join!(start.to_string(), " / ", end.to_string()).into()
}
}
}
}
// **< ItemJustify >*********************************************************************************
/// Alineación individual en el eje de columnas de un [`GridItem`](super::GridItem).
///
/// Análogo a [`DefaultJustify`], pero para un único elemento (`justify-self`) en vez de para todos
/// los elementos del contenedor. Es la misma relación que ya tienen [`align::Items`] y
/// [`align::ItemSelf`] en [`crate::html::align`].
///
/// [`DefaultJustify`]: super::DefaultJustify
/// [`align::Items`]: crate::html::align::Items
/// [`align::ItemSelf`]: crate::html::align::ItemSelf
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemJustify {
/// Por defecto, hereda la alineación del contenedor (`justify-self: auto` no explícito).
#[default]
Default,
/// Alinea el ítem al inicio de su columna (`justify-self: start`).
Start,
/// Alinea el ítem al final de su columna (`justify-self: end`).
End,
/// Centra el ítem en su columna (`justify-self: center`).
Center,
/// Estira el ítem para ocupar toda su columna (`justify-self: stretch`).
Stretch,
}
impl ItemJustify {
// Devuelve el valor CSS de `justify-self`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Start => "start".into(),
Self::End => "end".into(),
Self::Center => "center".into(),
Self::Stretch => "stretch".into(),
}
}
}

View file

@ -1,6 +1,7 @@
use crate::core::TypeInfo;
use crate::core::component::Context;
use crate::html::flex::{Flex, FlexItem};
use crate::html::flex::FlexItem;
use crate::html::grid::GridItem;
use crate::html::maud::{Escaper, RenderAttrs};
use crate::html::props::{PropsError, PropsExtra, PropsOp};
use crate::html::spacing::{Margin, Padding};
@ -191,6 +192,7 @@ pub struct Props {
attrs: Vec<(CowStr, CowStr)>,
extras: HashMap<&'static str, PropsExtra>,
flex_item: FlexItem,
grid_item: GridItem,
margin: Margin,
padding: Padding,
}
@ -216,10 +218,10 @@ impl Props {
}
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
/// 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`], aunque `FlexItem`, `Margin` y `Padding` pueden pasarse directamente gracias a
/// sus `From` hacia `PropsOp`.
/// indicada, incluido el posicionamiento Flexbox y Grid y el espaciado (`FlexItem`,
/// `GridItem`, `Margin`, `Padding`). El método recomendado para construir cada operación es
/// usar los constructores de [`PropsOp`], aunque `FlexItem`, `GridItem`, `Margin` y `Padding`
/// pueden pasarse directamente gracias a sus `From` hacia `PropsOp`.
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
match op.into() {
PropsOp::SetId(value) => {
@ -344,6 +346,9 @@ impl Props {
PropsOp::FlexItem(placement) => {
self.flex_item = self.flex_item.merge(placement);
}
PropsOp::GridItem(placement) => {
self.grid_item = self.grid_item.merge(placement);
}
PropsOp::Margin(margin) => {
self.margin = self.margin.merge(margin);
}
@ -439,14 +444,19 @@ impl Props {
self.attrs.is_empty()
}
/// Devuelve `true` si no hay ningún identificador, clases, estilos o atributos adicionales
/// definidos, sin tener en cuenta los valores extra.
/// Devuelve `true` si no hay ningún identificador, clases, estilos, atributos adicionales,
/// posicionamiento Flexbox/Grid ni espaciado definidos, sin tener en cuenta los valores extra
/// (que nunca se emiten en el HTML, ver [`extra()`](Self::extra)).
#[inline]
pub fn is_empty(&self) -> bool {
self.id.is_none()
&& self.classes.is_empty()
&& self.styles.is_empty()
&& self.attrs.is_empty()
&& self.flex_item == FlexItem::default()
&& self.grid_item == GridItem::default()
&& self.margin == Margin::default()
&& self.padding == Padding::default()
}
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
@ -572,11 +582,11 @@ 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 resuelven
/// [`FlexItem`], [`Margin`] y [`Padding`]: las clases que devuelven se añaden a las del propio
/// componente al escribir el atributo `class`.
/// [`FlexItem`], [`GridItem`], [`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.
/// [`Flex`] y [`Grid`] resuelven su propio posicionamiento (Flexbox/CSS Grid) directamente: no
/// son una propiedad de `Props`, son los únicos componentes que lo ofrecen.
///
/// ```rust
/// # use pagetop::prelude::*;
@ -587,49 +597,38 @@ impl Props {
/// ```
///
/// [`html!`]: crate::html::html
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`GridItem`]: crate::html::grid::GridItem
/// [`Margin`]: crate::html::spacing::Margin
/// [`Padding`]: crate::html::spacing::Padding
/// [`unpack_with_flex()`]: Self::unpack_with_flex
/// [`Flex`]: crate::base::component::Flex
/// [`Grid`]: crate::base::component::Grid
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,
}
}
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
///
/// 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
/// [`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,
}
self.unpack_with_classes(cx, String::new())
}
// **< Props PRIVATE >**************************************************************************
// Cola común de `unpack()` y del propio `Flex`/`Grid` (`base::component::flex`/`grid`):
// resuelve `FlexItem`/`GridItem`/`Margin`/`Padding` sobre las clases ya acumuladas (vacías, o
// con las clases del propio contenedor ya calculadas por el llamador) y envuelve el resultado
// para `html!`. `pub(crate)` porque `Flex` y `Grid` la usan directamente: cada uno construye
// sus propias clases CSS (no hay ningún valor intermedio que aplicar aquí).
pub(crate) fn unpack_with_classes<'a>(
&'a self,
cx: &mut Context,
mut classes: String,
) -> impl RenderAttrs + 'a {
self.flex_item.apply(cx, &mut classes);
self.grid_item.apply(cx, &mut classes);
self.margin.apply(cx, &mut classes);
self.padding.apply(cx, &mut classes);
PropsUnpack {
props: self,
classes,
}
}
fn apply_id(&mut self, id: &str) {
self.id = util::normalize_token(id);
}
@ -748,7 +747,7 @@ impl Props {
w.push('"');
}
}
// Clases propias del componente más las que aplica `Props::unpack()`/`unpack_with_flex()`.
// Clases propias del componente + las que aplica `Props::unpack()`/`unpack_with_classes()`.
let mut all_classes: Vec<&str> = self.classes.iter().map(String::as_str).collect();
all_classes.extend(classes.split_ascii_whitespace());
if let Some((first, rest)) = all_classes.split_first() {
@ -817,10 +816,11 @@ impl Props {
// **< PropsUnpack >********************************************************************************
// Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas
// 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()`).
// Devuelto por `Props::unpack()` y por `unpack_with_classes()` (usada por `Flex`/`Grid`, y por
// `unpack()` con un acumulador vacío). `classes` son las clases resueltas por `Flex`/`Grid` (sus
// métodos privados `flex_classes()`/`grid_classes()`) más las de `FlexItem::apply()`/
// `GridItem::apply()`/`Margin::apply()`/`Padding::apply()`, pendientes sólo de añadir a las del
// componente (ver `Props::write_attrs()`).
struct PropsUnpack<'a> {
props: &'a Props,
classes: String,

View file

@ -1,6 +1,7 @@
use crate::CowStr;
use crate::core::TypeInfo;
use crate::html::flex::FlexItem;
use crate::html::grid::GridItem;
use crate::html::props::extra::PropsExtra;
use crate::html::spacing::{Margin, Padding};
@ -41,9 +42,10 @@ use std::sync::Arc;
/// se interpretan como si
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
///
/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem, mientras que
/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem,
/// [`GridItem`](Self::GridItem) aplica un posicionamiento CSS Grid a nivel de ítem, y
/// [`Margin`](Self::Margin) y [`Padding`](Self::Padding) aplican márgenes y relleno interno; los
/// tres sobre cualquier componente.
/// cuatro sobre cualquier componente.
#[derive(Clone, Debug)]
pub enum PropsOp {
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
@ -120,21 +122,46 @@ pub enum PropsOp {
/// 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
/// añadirse como hijo de un contenedor Flex (por ejemplo `Button`, `Nav`, un componente de
/// terceros, incluso otro componente que sea, a su vez, un contenedor Flex para sus propios
/// hijos).
/// terceros, incluso otro componente que sea, a su vez, un contenedor Flex o [`Grid`] para sus
/// propios hijos).
///
/// No existe una variante equivalente `PropsOp::Flex` para el componente contenedor. No hace
/// falta porque los componentes contenedores, como `Container` o `Navbar`, ofrecen su propio
/// `with_flex()` tipado y con introspección (p. ej. [`Container::flex()`]).
/// No existe una variante equivalente `PropsOp::Flex` para el contenedor, por el mismo motivo
/// que no existe `PropsOp::Grid`. El único componente contenedor [`Flex`] ofrece sus propios
/// constructores y builders tipados, con introspección (p. ej. [`Flex::direction()`]). Esto no
/// afecta a [`GridItem`]; un componente puede llevar las dos variantes a la vez, con
/// independencia de qué tipo de contenedor sea él mismo para sus propios hijos.
///
/// [`Flex`]: crate::html::flex::Flex
/// [`Container::flex()`]: crate::base::component::Container::flex
/// [`Flex`]: crate::base::component::Flex
/// [`Flex::direction()`]: crate::base::component::Flex::direction
/// [`Grid`]: crate::base::component::Grid
/// [`GridItem`]: Self::GridItem
FlexItem(FlexItem),
/// Aplica un posicionamiento [`GridItem`] a un componente particular en un contenedor
/// [`Grid`]. Añade directamente sus estilos CSS Grid al propio componente, generando clases CSS
/// dinámicas.
///
/// Misma razón de ser que [`FlexItem`]; las propiedades de un ítem Grid tienen sentido sobre
/// **cualquier** componente que pueda añadirse como hijo de un contenedor Grid (por ejemplo
/// `Button`, `Nav`, un componente de terceros, incluso otro componente que sea, a su vez, un
/// contenedor Grid o [`Flex`] para sus propios hijos).
///
/// No existe una variante equivalente `PropsOp::Grid` para el contenedor, por el mismo motivo
/// que no existe `PropsOp::Flex`. El único componente contenedor [`Grid`] ofrece sus propios
/// constructores y builders tipados, con introspección (p. ej. [`Grid::columns()`]). Esto no
/// afecta a [`FlexItem`]; un componente puede llevar las dos variantes a la vez, con
/// independencia de qué tipo de contenedor sea él mismo para sus propios hijos.
///
/// [`Grid`]: crate::base::component::Grid
/// [`Grid::columns()`]: crate::base::component::Grid::columns
/// [`Flex`]: crate::base::component::Flex
/// [`FlexItem`]: Self::FlexItem
GridItem(GridItem),
/// 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.
/// Como [`FlexItem`](Self::FlexItem) y [`GridItem`](Self::GridItem), 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).
@ -281,6 +308,11 @@ impl PropsOp {
Self::FlexItem(placement)
}
/// Crea la variante [`GridItem`](Self::GridItem) con el posicionamiento indicado.
pub fn grid_item(placement: GridItem) -> Self {
Self::GridItem(placement)
}
/// Crea la variante [`Margin`](Self::Margin) con el margen indicado.
pub fn margin(margin: Margin) -> Self {
Self::Margin(margin)

View file

@ -1,12 +1,14 @@
//! 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.
//! Usado por [`flex`], [`grid`] y [`spacing`]. Los tres 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
//! [`grid`]: crate::html::grid
//! [`spacing`]: crate::html::spacing
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
@ -14,11 +16,21 @@ 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"`.
// Sustituye, en un valor CSS ya resuelto, los caracteres (`.`, `%`, ` `, `/`) que no podrían usarse
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"`, `"33.3333%"` quedaría en
// `"33_3333pct"`, `"1fr 1fr 200px"` (varias pistas de `grid::Tracks`) dejaría
// `"1fr-1fr-200px"`, y `"2 / 4"` (un rango de `grid::ItemPlacement`) quedaría como `"2---4"`.
pub(crate) fn value_to_token(value: &str) -> String {
value.replace('.', "_").replace('%', "pct")
let mut token = String::with_capacity(value.len());
for c in value.chars() {
match c {
'.' => token.push('_'),
'%' => token.push_str("pct"),
' ' | '/' => token.push('-'),
c => token.push(c),
}
}
token
}
// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada

View file

@ -26,7 +26,7 @@ use crate::{AutoDefault, Getters, builder_impl, util};
/// );
/// ```
///
/// [`Flex`]: crate::html::flex::Flex
/// [`Flex`]: crate::base::component::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct Margin {