(pagetop): Añade puntos de corte a Flex/FlexItem

- `Breakpoint` gana variante `Xxxl` y métodos `name()`/`min_width()`/
  `resolved()`, para consultarse contra el tema activo.
- `Theme::breakpoint_entry()` sustituye a `breakpoint_min_width()`:
  devuelve un `BreakpointEntry` con nombre y ancho mínimo.
- Nuevo `Responsive<T>`, valor en cascada mobile-first por punto de
  corte.
- Ejemplo `examples/intro-responsive.rs` con los patrones de uso.
This commit is contained in:
Manuel Cillero 2026-09-12 11:13:19 +02:00
parent 8577ca8a59
commit 0b8f3f3000
22 changed files with 1257 additions and 359 deletions

View file

@ -142,13 +142,14 @@ impl ResponsiveStyles {
/// Renderiza las declaraciones acumuladas como texto CSS.
///
/// Emite primero las declaraciones sin punto de corte (`None`), siempre sin envoltorio. Luego
/// recorre los puntos de corte reales en orden *mobile-first* (`Xs` a `Xxl`, omitiendo los que
/// recorre los puntos de corte reales en orden *mobile-first* (`Xs` a `Xxxl`, omitiendo los que
/// no tengan ninguna declaración) y, para cada uno, agrupa las reglas de todas sus entradas
/// (`.clases { propiedad: valor; ... }`). Si el ancho mínimo resuelto por el tema activo
/// (`.clases{propiedad:valor;...}`). Si el ancho mínimo resuelto por el tema activo
/// ([`Breakpoint::min_width()`]) es una cadena vacía, las reglas se emiten tal cual, sin punto
/// de corte real; en cualquier otro caso se envuelven en `@media (min-width: ...)`.
/// de corte real; en cualquier otro caso se envuelven en `@media(min-width:...)`.
///
/// El resultado no contiene saltos de línea.
/// El resultado no contiene espacios ni saltos de línea salvo los que pueda llevar el propio
/// valor de una declaración (p. ej. `font-family: "Segoe UI", sans-serif`).
pub fn render(&self, cx: &Context) -> Markup {
let mut css = String::new();
@ -164,11 +165,11 @@ impl ResponsiveStyles {
css.push_str(&rules);
} else {
css.push_str(&util::join!(
"@media (min-width: ",
"@media(min-width:",
min_width,
") { ",
"){",
rules,
" }"
"}"
));
}
}
@ -176,8 +177,8 @@ impl ResponsiveStyles {
html! { (PreEscaped(css)) }
}
// Construye, concatenadas y sin separador, las reglas CSS (`.clases { propiedad: valor; ... }`)
// de todas las entradas del punto de corte indicado.
// Construye, concatenadas y sin separador, las reglas CSS (`.clases{propiedad:valor;...}`) de
// todas las entradas del punto de corte indicado.
fn render_rules(&self, breakpoint: Option<Breakpoint>) -> String {
let mut rules = String::new();
for (_, classes, styles) in self
@ -188,10 +189,10 @@ impl ResponsiveStyles {
let selector = classes.replace(' ', ".");
let declarations = styles
.iter()
.map(|(property, value)| util::join!(property.as_ref(), ": ", value.as_ref()))
.map(|(property, value)| util::join!(property.as_ref(), ":", value.as_ref()))
.collect::<Vec<_>>()
.join("; ");
rules.push_str(&util::join!(".", selector, " { ", declarations, " }"));
.join(";");
rules.push_str(&util::join!(".", selector, "{", declarations, "}"));
}
rules
}

View file

@ -5,24 +5,30 @@
//! ofrecen su propio `with_flex()`, como [`Container`] o [`Navbar`].
//!
//! [`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). Al poder
//! acabar aplicándose sobre cualquier componente (no sólo los que ofrecen `with_flex()`), no tiene
//! un builder propio: se aplica con [`PropsOp::flex_item()`] sobre el `with_prop()` que ya expone
//! cualquier componente.
//! 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 con [`PropsOp::flex_item()`] sobre el `with_prop()`
//! que normalmente ya expone cualquier componente.
//!
//! # Un entorno autosuficiente
//! # Un entorno nativo autosuficiente
//!
//! Toda la configuración de `Flex`/`FlexItem` se resuelve con estilos en línea (`style="..."`),
//! nunca como clases CSS (consulta el propio [`Flex`] para ver el porqué). Los estilos en línea
//! tienen la especificidad más alta que existe en CSS, salvo `!important`, así que ningún framework
//! CSS de terceros, ni el CSS de la propia aplicación, puede sobrescribirlo por accidente. 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.
//! Toda la configuración de `Flex`/`FlexItem` se resuelve generando clases CSS dinámicamente y de
//! manera independiente a cualquier tema o framework CSS. 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. Las declaraciones correspondientes se registran
//! vía [`AssetsOp::AddResponsiveStyle`] y se renderizan como reglas en el `<head>` del 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.
//!
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
//! [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
//! [`Container`]: crate::base::component::Container
//! [`Navbar`]: crate::base::component::Navbar
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
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};
@ -35,3 +41,83 @@ pub use container::Flex;
mod item;
pub use item::FlexItem;
// **< Flex / FlexItem PRIVATE >********************************************************************
// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría
// como `"33_3333pct"`.
fn value_to_token(value: &str) -> String {
value.replace('.', "_").replace('%', "pct")
}
// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada
// con un espacio de las que ya hubiera. Recibe un `BreakpointEntry` ya resuelto (ver
// `Breakpoint::resolved()`) y extrae aquí el `Breakpoint` que `AddResponsiveStyle` necesita, que
// puede ser nulo si aplica siempre.
//
// La clase se copia al acumulador y se mueve al `AssetsOp`, sin clonarla. Recibirla como `CowStr`
// permite además que las clases fijas, las que no dependen de ningún punto de corte, lleguen como
// `&'static str` sin asignar memoria.
fn styles(
cx: &mut Context,
classes: &mut String,
entry: Option<BreakpointEntry>,
class: CowStr,
property: &'static str,
value: CowStr,
) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(&class);
cx.alter_assets(AssetsOp::AddResponsiveStyle(
entry.map(|e| e.breakpoint),
class,
property.into(),
value,
));
}
// Nombre de clase según el punto de corte: `prefix` ya incluye el guion bajo final antes del valor
// (p. ej. `"_flex-direction_"`), y `entry` añade su sufijo si aplica (`"_flex-direction_row_md_"`),
// ya resuelto para el tema activo (ver `Breakpoint::resolved()`).
macro_rules! responsive_class {
($prefix:expr, $token:expr, $entry:expr) => {
match $entry {
None => util::join!($prefix, $token, "_"),
Some(entry) => util::join!($prefix, $token, "_", entry.name, "_"),
}
};
}
use responsive_class;
// Recorre las entradas para una propiedad `Responsive<T>` cuyo valor CSS es un único `T::value()`,
// generando y registrando (vía `styles()`) una clase por punto de corte con valor.
//
// La forma con el marcador final `val` es para propiedades cuyo valor puede contener `.`/`%` (como
// `ItemSize` o `ItemOffset` en `FlexItem`) y necesitan pasar por `value_to_token()`.
macro_rules! apply {
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal) => {
for (bp, value) in $field.by_breakpoint() {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved($cx);
let class = responsive_class!($prefix, value, entry);
styles($cx, $classes, entry, class.into(), $property, value);
}
}
};
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal, val) => {
for (bp, value) in $field.by_breakpoint() {
let value = value.value();
if !value.is_empty() {
let entry = bp.resolved($cx);
let class = responsive_class!($prefix, value_to_token(&value), entry);
styles($cx, $classes, entry, class.into(), $property, value);
}
}
};
}
use apply;

View file

@ -1,23 +1,39 @@
use crate::html::flex::props_container::{
Align, AlignContent, Behavior, ContentJustify, Direction, Gap,
};
use crate::html::props::{Props, PropsOp};
use crate::{AutoDefault, Getters, builder_impl};
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 con estilos en línea (`display`, `flex-direction`, `flex-wrap`, `justify-content`,
/// `align-items`, `align-content`, `gap`), nunca como clases CSS. Son propiedades estándar 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.
/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
/// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía
/// [`AssetsOp::AddResponsiveStyle`] 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.
///
/// Esto tiene además una consecuencia práctica; los estilos en línea tienen la especificidad más
/// alta que existe en CSS, salvo `!important`. Ningún *framework* CSS de terceros, ni el CSS de la
/// propia aplicación, puede sobrescribir por accidente lo que `Flex` aplica. Es un mecanismo
/// autosuficiente que funciona igual conviva con quien conviva en la misma página, sin coordinar
/// nombres de clase ni orden de carga de hojas de estilo con nadie.
/// 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::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
///
/// # Ejemplo
///
@ -26,7 +42,7 @@ use crate::{AutoDefault, Getters, builder_impl};
///
/// let actions = Container::new()
/// .with_flex(
/// Flex::row()
/// Flex::new()
/// .with_justify(flex::ContentJustify::End)
/// .with_align(flex::Align::Center)
/// .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
@ -36,38 +52,66 @@ use crate::{AutoDefault, Getters, builder_impl};
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct Flex {
/// Devuelve la dirección del eje principal.
// 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: Direction,
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea.
direction: Responsive<Direction>,
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea, por punto de
/// corte.
#[getters(copy)]
wrap: Behavior,
/// Devuelve la alineación de los elementos en el eje principal.
wrap: Responsive<Behavior>,
/// Devuelve la alineación de los elementos en el eje principal, por punto de corte.
#[getters(copy)]
justify: ContentJustify,
/// Devuelve la alineación de los elementos en el eje transversal.
justify: Responsive<ContentJustify>,
/// Devuelve la alineación de los elementos en el eje transversal, por punto de corte.
#[getters(copy)]
align: Align,
/// Devuelve la alineación de las líneas cuando hay más de una.
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: AlignContent,
/// Devuelve el espaciado entre elementos.
align_content: Responsive<AlignContent>,
/// Devuelve el espaciado entre elementos, por punto de corte.
#[getters(copy)]
gap: Gap,
gap: Responsive<Gap>,
}
#[builder_impl]
impl Flex {
/// Crea una configuración Flex para disponer los elementos en fila (comportamiento por
/// defecto).
pub fn row() -> Self {
Self::default()
/// 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()
}
}
/// Crea una configuración Flex para disponer los elementos en columna.
pub fn column() -> Self {
/// 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 {
direction: Direction::Column,
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()
}
}
@ -75,43 +119,106 @@ impl Flex {
// **< Flex BUILDER >***************************************************************************
/// Establece la dirección del eje principal.
pub fn with_direction(mut self, direction: Direction) -> Self {
self.direction = direction;
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 = wrap;
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 = justify;
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 = align;
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 = align_content;
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 = gap;
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
@ -119,19 +226,53 @@ impl Flex {
/// 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.
pub fn apply_to(self, props: &mut Props) {
props.alter_prop(PropsOp::add_style("display", "flex"));
for (property, value) in [
("flex-direction", self.direction.value()),
("flex-wrap", self.wrap.value()),
("justify-content", self.justify.value()),
("align-items", self.align.value()),
("align-content", self.align_content.value()),
] {
props.alter_prop(PropsOp::add_style(property, value));
}
for (property, value) in self.gap.styles() {
props.alter_prop(PropsOp::add_style(property, value));
///
/// 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 super::{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,17 +1,15 @@
use crate::html::flex::props_item::{
ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize,
};
use crate::html::props::{Props, PropsOp};
use crate::{AutoDefault, Getters, builder_impl};
// **< FlexItem >***********************************************************************************
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::{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`]), ancho ([`ItemSize`]) y
/// alineación individual ([`ItemAlign`]), orden visual ([`ItemOrder`]), tamaño ([`ItemSize`]) y
/// desplazamiento ([`ItemOffset`]).
///
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
@ -20,7 +18,8 @@ use crate::{AutoDefault, Getters, builder_impl};
/// exponer cualquier componente.
///
/// Con [`ItemSize`] y [`ItemOffset`] se pueden modelar rejillas de columnas fijas sobre Flexbox,
/// combinando un ancho en fracción del contenedor con un desplazamiento lateral cuando se necesite.
/// combinando un tamaño en fracción del contenedor con un desplazamiento lateral cuando se
/// necesite.
///
/// # Ejemplo
///
@ -28,7 +27,7 @@ use crate::{AutoDefault, Getters, builder_impl};
/// use pagetop::prelude::*;
///
/// // Crece para ocupar el espacio sobrante, partiendo de ancho cero.
/// let title = Button::plain(Lc::n("Panel")).with_prop(PropsOp::flex_item(
/// let panel = Button::plain(Lc::n("Panel")).with_prop(PropsOp::flex_item(
/// FlexItem::new()
/// .with_grow(flex::ItemGrow::Is1)
/// .with_size(flex::ItemSize::Custom(UnitValue::Zero)),
@ -43,24 +42,24 @@ use crate::{AutoDefault, Getters, builder_impl};
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct FlexItem {
/// Devuelve el factor de crecimiento.
/// Devuelve el factor de crecimiento, por punto de corte.
#[getters(copy)]
grow: ItemGrow,
/// Devuelve el factor de reducción.
grow: Responsive<ItemGrow>,
/// Devuelve el factor de reducción, por punto de corte.
#[getters(copy)]
shrink: ItemShrink,
/// Devuelve la alineación individual en el eje transversal.
shrink: Responsive<ItemShrink>,
/// Devuelve la alineación individual en el eje transversal, por punto de corte.
#[getters(copy)]
align_self: ItemAlign,
/// Devuelve la posición en el orden visual.
align_self: Responsive<ItemAlign>,
/// Devuelve la posición en el orden visual, por punto de corte.
#[getters(copy)]
order: ItemOrder,
/// Devuelve el ancho como fracción del contenedor.
order: Responsive<ItemOrder>,
/// Devuelve el tamaño como fracción del contenedor, por punto de corte.
#[getters(copy)]
size: ItemSize,
/// Devuelve el desplazamiento respecto al inicio del contenedor.
size: Responsive<ItemSize>,
/// Devuelve el desplazamiento respecto al inicio del contenedor, por punto de corte.
#[getters(copy)]
offset: ItemOffset,
offset: Responsive<ItemOffset>,
}
#[builder_impl]
@ -70,80 +69,19 @@ impl FlexItem {
Self::default()
}
// **< FlexItem BUILDER >***********************************************************************
/// Establece el factor de crecimiento.
pub fn with_grow(mut self, grow: ItemGrow) -> Self {
self.grow = grow;
self
}
/// Establece el factor de reducción.
pub fn with_shrink(mut self, shrink: ItemShrink) -> Self {
self.shrink = shrink;
self
}
/// Establece la alineación individual en el eje transversal.
pub fn with_align_self(mut self, align_self: ItemAlign) -> Self {
self.align_self = align_self;
self
}
/// Establece la posición en el orden visual.
pub fn with_order(mut self, order: ItemOrder) -> Self {
self.order = order;
self
}
/// Establece el ancho como una fracción del contenedor (`flex-basis`). No fuerza
/// [`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)).
pub fn with_size(mut self, size: ItemSize) -> Self {
self.size = size;
self
}
/// Establece el desplazamiento respecto al inicio del contenedor (`margin-inline-start`). No
/// tiene relación con [`push_end()`](Self::push_end) aunque aplican la misma propiedad CSS para
/// casos de uso distintos.
pub fn with_offset(mut self, offset: ItemOffset) -> Self {
self.offset = offset;
self
}
}
impl FlexItem {
// Aplica esta configuración a un Props como declaraciones de estilo en línea.
pub(crate) fn apply_to(self, props: &mut Props) {
for (property, value) in [
("flex-grow", self.grow.value()),
("flex-shrink", self.shrink.value()),
("align-self", self.align_self.value()),
("order", self.order.value()),
("flex-basis", self.size.value()),
("margin-inline-start", self.offset.value()),
] {
props.alter_prop(PropsOp::add_style(property, value));
}
}
/// Separa un elemento (y los que le sigan en el mismo eje principal) del resto, empujándolo
/// hacia el extremo final de un contenedor flex.
/// Crea una configuración de ítem que empuja el elemento, y los que le sigan, hacia el extremo
/// final de un contenedor flex en fila.
///
/// Se resuelve siempre como margen inicial automático (`margin-inline-start: auto`) en línea,
/// igual que el resto de facetas de `FlexItem`. Es el mecanismo estándar de Flexbox para, por
/// ejemplo, separar dos menús dentro de una misma [`Navbar`](crate::base::component::Navbar)
/// -- uno pegado al inicio, el siguiente empujado al final -- sin que el contenedor necesite
/// conocer ninguna distinción entre sus elementos.
/// Aplica `margin-inline-start: auto`, un margen automático que absorbe todo el espacio libre
/// que quede antes del elemento en el eje de escritura. Con la dirección por defecto
/// ([`Direction::Row`](super::Direction::Row)) ese eje es el principal, de ahí el efecto de
/// empuje. En un contenedor en columna, en cambio, ese eje es el transversal: el margen ya no
/// empuja nada, sólo desplaza ese elemento hacia el final de la línea (a la derecha si se
/// escribe de izquierda a derecha).
///
/// No forma parte de los campos de `FlexItem` (no se combina con `grow`/`shrink`/`align_self`/
/// `order`/`size`/`offset` en una misma llamada): es una función asociada independiente porque
/// resuelve un caso de uso completo por sí sola, con una sola línea, y vive aquí -- en vez de
/// como función suelta del módulo `flex` -- para dejar claro que es una operación de **ítem**,
/// no de contenedor.
/// Es el mecanismo estándar de Flexbox para, por ejemplo, separar dos menús dentro de un mismo
/// [`Navbar`](crate::base::component::Navbar) (uno pegado al inicio, el siguiente empujado al
/// final) sin que el contenedor necesite conocer ninguna distinción entre sus elementos.
///
/// # Ejemplo
///
@ -151,12 +89,139 @@ impl FlexItem {
/// use pagetop::prelude::*;
///
/// let user_menu = Nav::new()
/// .with_prop(FlexItem::push_end())
/// .with_prop(FlexItem::push_end().into())
/// .with_item(nav::Item::link(Lc::n("Profile"), "/profile"))
/// .with_item(nav::Item::link(Lc::n("Sign out"), "/sign-out"));
/// ```
pub fn push_end() -> PropsOp {
PropsOp::add_style("margin-inline-start", "auto")
pub fn push_end() -> Self {
Self::new().with_offset(ItemOffset::Auto)
}
// **< FlexItem BUILDER >***********************************************************************
/// Establece el factor de crecimiento.
pub fn with_grow(mut self, grow: ItemGrow) -> Self {
self.grow = self.grow.set(grow);
self
}
/// Establece el factor de crecimiento, a partir del punto de corte indicado.
pub fn with_grow_at(mut self, bp: Breakpoint, grow: ItemGrow) -> Self {
self.grow = self.grow.set_at(bp, grow);
self
}
/// Establece el factor de reducción.
pub fn with_shrink(mut self, shrink: ItemShrink) -> Self {
self.shrink = self.shrink.set(shrink);
self
}
/// Establece el factor de reducción, a partir del punto de corte indicado.
pub fn with_shrink_at(mut self, bp: Breakpoint, shrink: ItemShrink) -> Self {
self.shrink = self.shrink.set_at(bp, shrink);
self
}
/// Establece la alineación individual en el eje transversal.
pub fn with_align_self(mut self, align_self: ItemAlign) -> 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 {
self.align_self = self.align_self.set_at(bp, align_self);
self
}
/// Establece la posición en el orden visual.
pub fn with_order(mut self, order: ItemOrder) -> Self {
self.order = self.order.set(order);
self
}
/// Establece la posición en el orden visual, a partir del punto de corte indicado.
pub fn with_order_at(mut self, bp: Breakpoint, order: ItemOrder) -> Self {
self.order = self.order.set_at(bp, order);
self
}
/// Establece el tamaño como una fracción del contenedor (`flex-basis`). No fuerza
/// [`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)).
pub fn with_size(mut self, size: ItemSize) -> Self {
self.size = self.size.set(size);
self
}
/// Establece el tamaño como una fracción del contenedor (`flex-basis`), a partir del punto de
/// corte indicado.
pub fn with_size_at(mut self, bp: Breakpoint, size: ItemSize) -> Self {
self.size = self.size.set_at(bp, size);
self
}
/// Establece el desplazamiento respecto al inicio del contenedor (`margin-inline-start`).
/// [`push_end()`](Self::push_end) fija este mismo campo a [`ItemOffset::Auto`]; combinar los
/// dos deja el que se aplique en último lugar.
pub fn with_offset(mut self, offset: ItemOffset) -> Self {
self.offset = self.offset.set(offset);
self
}
/// Establece el desplazamiento respecto al inicio del contenedor (`margin-inline-start`), a
/// partir del punto de corte indicado.
pub fn with_offset_at(mut self, bp: Breakpoint, offset: ItemOffset) -> Self {
self.offset = self.offset.set_at(bp, offset);
self
}
}
impl FlexItem {
/// Combina esta configuración con otra `FlexItem`, 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_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.
pub fn merge(mut self, item: FlexItem) -> Self {
self.grow = self.grow.merge(item.grow);
self.shrink = self.shrink.merge(item.shrink);
self.align_self = self.align_self.merge(item.align_self);
self.order = self.order.merge(item.order);
self.size = self.size.merge(item.size);
self.offset = self.offset.merge(item.offset);
self
}
/// 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.
///
/// 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.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
use super::{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");
apply!(cx, classes, self.align_self, "_flex-item-align_", "align-self");
apply!(cx, classes, self.order, "_flex-item-order_", "order");
apply!(cx, classes, self.size, "_flex-item-basis_", "flex-basis", val);
apply!(cx, classes, self.offset, "_flex-item-offset_", "margin-inline-start", val);
}
}

View file

@ -207,28 +207,21 @@ pub enum Gap {
}
impl Gap {
// Declaraciones de estilo (propiedad, valor) para este espaciado; vacío si no hay ninguna
// medible (`UnitValue::None`/`UnitValue::Auto` no producen ningún estilo).
pub(super) fn styles(self) -> Vec<(&'static str, CowStr)> {
// 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 => Vec::new(),
Self::Both(value) => {
if value.is_measurable() {
vec![("gap", value.into())]
} else {
Vec::new()
}
}
Self::Distinct { row, column } => {
let mut styles = Vec::new();
if row.is_measurable() {
styles.push(("row-gap", row.into()));
}
if column.is_measurable() {
styles.push(("column-gap", column.into()));
}
styles
}
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

@ -74,6 +74,10 @@ pub enum ItemOffset {
/// Por defecto, sin desplazamiento (`margin-inline-start: 0` no explícito).
#[default]
None,
/// Empuja el ítem, y los que le sigan, hacia el extremo final de un contenedor en fila
/// (`margin-inline-start: auto`). Ver [`FlexItem::push_end()`](super::FlexItem::push_end), que
/// detalla el comportamiento cuando el contenedor está en columna.
Auto,
/// Se desplaza el 10% del ancho del contenedor (`margin-inline-start: 10%`).
Percent10,
/// Se desplaza el 20% del ancho del contenedor (`margin-inline-start: 20%`).
@ -106,6 +110,7 @@ impl ItemOffset {
pub(super) fn value(self) -> CowStr {
match self {
Self::None => "".into(),
Self::Auto => "auto".into(),
Self::Percent10 => "10%".into(),
Self::Percent20 => "20%".into(),
Self::Percent25 => "25%".into(),
@ -217,10 +222,14 @@ impl ItemShrink {
// **< ItemSize >***********************************************************************************
/// Ancho en [`FlexItem`](super::FlexItem) para un ítem como fracción del contenedor.
/// Tamaño en [`FlexItem`](super::FlexItem) para un ítem como fracción del contenedor.
///
/// Dimensiona el eje principal (`flex-basis`). Con la dirección por defecto
/// ([`Direction::Row`](super::Direction::Row)) fija el ancho, y en un contenedor en columna fija el
/// alto. El resto de esta documentación describe el caso en fila, que es el habitual.
///
/// Permite maquetar rejillas de columnas fijas. Un ítem con [`ItemSize::Percent33`] ocupa un tercio
/// del ancho del contenedor con independencia de su contenido.
/// del contenedor con independencia de su contenido.
///
/// # Cómo combinarlo con `Gap`
///
@ -253,31 +262,31 @@ pub enum ItemSize {
/// Por defecto, el tamaño se calcula según el contenido (`flex-basis: auto` no explícito).
#[default]
Default,
/// Ocupa el 10% del ancho del contenedor (`flex-basis: 10%`).
/// Ocupa el 10% del contenedor (`flex-basis: 10%`).
Percent10,
/// Ocupa el 20% del ancho del contenedor (`flex-basis: 20%`).
/// Ocupa el 20% del contenedor (`flex-basis: 20%`).
Percent20,
/// Ocupa el 25% del ancho del contenedor (`flex-basis: 25%`).
/// Ocupa el 25% del contenedor (`flex-basis: 25%`).
Percent25,
/// Ocupa un tercio del ancho del contenedor (`flex-basis: 33.3333%`).
/// Ocupa un tercio del contenedor (`flex-basis: 33.3333%`).
Percent33,
/// Ocupa el 40% del ancho del contenedor (`flex-basis: 40%`).
/// Ocupa el 40% del contenedor (`flex-basis: 40%`).
Percent40,
/// Ocupa la mitad del ancho del contenedor (`flex-basis: 50%`).
/// Ocupa la mitad del contenedor (`flex-basis: 50%`).
Percent50,
/// Ocupa el 60% del ancho del contenedor (`flex-basis: 60%`).
/// Ocupa el 60% del contenedor (`flex-basis: 60%`).
Percent60,
/// Ocupa dos tercios del ancho del contenedor (`flex-basis: 66.6667%`).
/// Ocupa dos tercios del contenedor (`flex-basis: 66.6667%`).
Percent66,
/// Ocupa el 75% del ancho del contenedor (`flex-basis: 75%`).
/// Ocupa el 75% del contenedor (`flex-basis: 75%`).
Percent75,
/// Ocupa el 80% del ancho del contenedor (`flex-basis: 80%`).
/// Ocupa el 80% del contenedor (`flex-basis: 80%`).
Percent80,
/// Ocupa el 90% del ancho del contenedor (`flex-basis: 90%`).
/// Ocupa el 90% del contenedor (`flex-basis: 90%`).
Percent90,
/// Ocupa el 100% del ancho del contenedor (`flex-basis: 100%`).
/// Ocupa el 100% del contenedor (`flex-basis: 100%`).
Percent100,
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un ancho fijo en píxeles).
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un tamaño fijo en píxeles).
Custom(UnitValue),
}

View file

@ -1,5 +1,6 @@
use crate::core::TypeInfo;
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::{AutoDefault, CowStr, builder_impl, trace, util};
@ -188,6 +189,7 @@ pub struct Props {
styles: Vec<(CowStr, CowStr)>,
attrs: Vec<(CowStr, CowStr)>,
extras: HashMap<&'static str, PropsExtra>,
flex_item: FlexItem,
}
#[builder_impl]
@ -335,7 +337,7 @@ impl Props {
self.extras.remove(key);
}
PropsOp::FlexItem(placement) => {
placement.apply_to(self);
self.flex_item = self.flex_item.merge(placement);
}
}
self
@ -578,9 +580,11 @@ impl Props {
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`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);
PropsUnpack {
props: self,
classes: self.flex_item.apply(cx),
classes,
}
}
@ -597,9 +601,12 @@ impl Props {
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
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);
PropsUnpack {
props: self,
classes: util::join_pair!(flex.apply(cx), " ", self.flex_item.apply(cx)),
classes,
}
}