(pagetop): Añade Flex/FlexItem para usar Flexbox

Container y Navbar lo adoptan vía `with_flex()`/`PropsOp::flex_item()`.
Y se elimina `ButtonSet` porque su funcionalidad queda cubierta por este
mecanismo más general. Incluye el ejemplo `examples/intro-flex.rs` con
los patrones de uso.
This commit is contained in:
Manuel Cillero 2026-09-04 01:04:53 +02:00
parent 4e4fdf7b10
commit 17e16652e4
26 changed files with 2023 additions and 193 deletions

37
src/html/flex.rs Normal file
View file

@ -0,0 +1,37 @@
//! 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`].
//!
//! [`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.
//!
//! # Un entorno 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.
//!
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
//! [`Container`]: crate::base::component::Container
//! [`Navbar`]: crate::base::component::Navbar
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
mod props_container;
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
mod props_item;
pub use props_item::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
mod container;
pub use container::Flex;
mod item;
pub use item::FlexItem;

137
src/html/flex/container.rs Normal file
View file

@ -0,0 +1,137 @@
use crate::html::flex::props_container::{
Align, AlignContent, Behavior, ContentJustify, Direction, Gap,
};
use crate::html::props::{Props, PropsOp};
use crate::{AutoDefault, Getters, builder_impl};
// **< 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.
///
/// 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.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let actions = Container::new()
/// .with_flex(
/// Flex::row()
/// .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 {
/// Devuelve la dirección del eje principal.
#[getters(copy)]
direction: Direction,
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea.
#[getters(copy)]
wrap: Behavior,
/// Devuelve la alineación de los elementos en el eje principal.
#[getters(copy)]
justify: ContentJustify,
/// Devuelve la alineación de los elementos en el eje transversal.
#[getters(copy)]
align: Align,
/// Devuelve la alineación de las líneas cuando hay más de una.
#[getters(copy)]
align_content: AlignContent,
/// Devuelve el espaciado entre elementos.
#[getters(copy)]
gap: 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()
}
/// Crea una configuración Flex para disponer los elementos en columna.
pub fn column() -> Self {
Self {
direction: Direction::Column,
..Default::default()
}
}
// **< Flex BUILDER >***************************************************************************
/// Establece la dirección del eje principal.
pub fn with_direction(mut self, direction: Direction) -> Self {
self.direction = direction;
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
}
/// Establece la alineación de los elementos en el eje principal.
pub fn with_justify(mut self, justify: ContentJustify) -> Self {
self.justify = 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
}
/// 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
}
/// Establece el espaciado entre elementos.
pub fn with_gap(mut self, gap: Gap) -> Self {
self.gap = gap;
self
}
}
impl Flex {
/// 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.
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));
}
}
}

167
src/html/flex/item.rs Normal file
View file

@ -0,0 +1,167 @@
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 >***********************************************************************************
/// 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
/// desplazamiento ([`ItemOffset`]).
///
/// 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 con [`PropsOp::flex_item()`] sobre el `with_prop()` que suele
/// 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.
///
/// # Ejemplo
///
/// ```rust,no_run
/// 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(
/// FlexItem::new()
/// .with_grow(flex::ItemGrow::Is1)
/// .with_size(flex::ItemSize::Custom(UnitValue::Zero)),
/// ));
///
/// // Ocupa un tercio del ancho del contenedor, desplazado otro tercio desde el inicio.
/// let column = Container::new().with_prop(PropsOp::flex_item(
/// FlexItem::new()
/// .with_size(flex::ItemSize::Percent33)
/// .with_offset(flex::ItemOffset::Percent33),
/// ));
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct FlexItem {
/// Devuelve el factor de crecimiento.
#[getters(copy)]
grow: ItemGrow,
/// Devuelve el factor de reducción.
#[getters(copy)]
shrink: ItemShrink,
/// Devuelve la alineación individual en el eje transversal.
#[getters(copy)]
align_self: ItemAlign,
/// Devuelve la posición en el orden visual.
#[getters(copy)]
order: ItemOrder,
/// Devuelve el ancho como fracción del contenedor.
#[getters(copy)]
size: ItemSize,
/// Devuelve el desplazamiento respecto al inicio del contenedor.
#[getters(copy)]
offset: ItemOffset,
}
#[builder_impl]
impl FlexItem {
/// Crea una configuración de ítem con todos los valores por defecto.
pub fn new() -> Self {
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.
///
/// 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.
///
/// 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.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let user_menu = Nav::new()
/// .with_prop(FlexItem::push_end())
/// .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")
}
}
impl From<FlexItem> for PropsOp {
fn from(item: FlexItem) -> Self {
Self::flex_item(item)
}
}

View file

@ -0,0 +1,234 @@
//! 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).
///
/// 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é.
#[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`
/// no explícito).
#[default]
NoWrap,
/// Se dividen en varias líneas cuando no caben en una sola (`flex-wrap: wrap`).
Wrap,
/// Igual que [`Behavior::Wrap`], pero las líneas se apilan en orden inverso
/// (`flex-wrap: wrap-reverse`).
WrapReverse,
}
impl Behavior {
// Devuelve el valor CSS de `flex-wrap`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::NoWrap => "".into(),
Self::Wrap => "wrap".into(),
Self::WrapReverse => "wrap-reverse".into(),
}
}
}
// **< ContentJustify >*****************************************************************************
/// Alineación de los elementos en el eje principal de un contenedor [`Flex`](super::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,
/// 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`).
End,
/// Centra los elementos en el eje principal (`justify-content: center`).
Center,
/// Reparte el espacio sobrante entre los elementos (`justify-content: space-between`).
SpaceBetween,
/// Reparte el espacio sobrante alrededor de cada elemento (`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(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(),
}
}
}
// **< Direction >**********************************************************************************
/// Dirección del eje principal de un contenedor [`Flex`](super::Flex).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Direction {
/// Por defecto, los elementos se disponen en fila, de izquierda a derecha
/// (`flex-direction: row` no explícito).
#[default]
Row,
/// Los elementos se disponen en fila, de derecha a izquierda (`flex-direction: row-reverse`).
RowReverse,
/// Los elementos se disponen en columna, de arriba abajo (`flex-direction: column`).
Column,
/// Los elementos se disponen en columna, de abajo arriba (`flex-direction: column-reverse`).
ColumnReverse,
}
impl Direction {
// Devuelve el valor CSS de `flex-direction`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Row => "".into(),
Self::RowReverse => "row-reverse".into(),
Self::Column => "column".into(),
Self::ColumnReverse => "column-reverse".into(),
}
}
}
// **< 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; 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)> {
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
}
}
}
}

304
src/html/flex/props_item.rs Normal file
View file

@ -0,0 +1,304 @@
//! 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.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemGrow {
/// Por defecto, no crece más allá de su tamaño base (`flex-grow: 0` no explícito).
#[default]
Default,
/// Crece para ocupar el espacio sobrante (`flex-grow: 1`).
Is1,
}
impl ItemGrow {
// Devuelve el valor CSS de `flex-grow`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Is1 => "1".into(),
}
}
}
// **< ItemOffset >*********************************************************************************
/// Desplazamiento en [`FlexItem`](super::FlexItem) para un ítem respecto al inicio del contenedor.
///
/// Junto con [`ItemSize`], permite maquetar rejillas de columnas fijas sobre Flexbox. Un ítem con
/// [`ItemOffset::Percent33`] deja libre el primer tercio del contenedor antes de empezar. No tiene
/// relación con [`FlexItem::push_end()`](super::FlexItem::push_end). Ambos aplican la misma
/// propiedad CSS (`margin-inline-start`), pero para casos de uso distintos (un desplazamiento fijo
/// en fracción del contenedor, frente a "ocupa todo el espacio sobrante"); combinarlos no tiene
/// sentido, y si se aplican los dos, gana el último que se llame.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemOffset {
/// Por defecto, sin desplazamiento (`margin-inline-start: 0` no explícito).
#[default]
None,
/// 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%`).
Percent20,
/// Se desplaza el 25% del ancho del contenedor (`margin-inline-start: 25%`).
Percent25,
/// Se desplaza un tercio del ancho del contenedor (`margin-inline-start: 33.3333%`).
Percent33,
/// Se desplaza el 40% del ancho del contenedor (`margin-inline-start: 40%`).
Percent40,
/// Se desplaza la mitad del ancho del contenedor (`margin-inline-start: 50%`).
Percent50,
/// Se desplaza el 60% del ancho del contenedor (`margin-inline-start: 60%`).
Percent60,
/// Se desplaza dos tercios del ancho del contenedor (`margin-inline-start: 66.6667%`).
Percent66,
/// Se desplaza el 75% del ancho del contenedor (`margin-inline-start: 75%`).
Percent75,
/// Se desplaza el 80% del ancho del contenedor (`margin-inline-start: 80%`).
Percent80,
/// Se desplaza el 90% del ancho del contenedor (`margin-inline-start: 90%`).
Percent90,
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un desplazamiento fijo en
/// píxeles).
Custom(UnitValue),
}
impl ItemOffset {
// Devuelve el valor CSS de `margin-inline-start`, o cadena vacía para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::None => "".into(),
Self::Percent10 => "10%".into(),
Self::Percent20 => "20%".into(),
Self::Percent25 => "25%".into(),
Self::Percent33 => "33.3333%".into(),
Self::Percent40 => "40%".into(),
Self::Percent50 => "50%".into(),
Self::Percent60 => "60%".into(),
Self::Percent66 => "66.6667%".into(),
Self::Percent75 => "75%".into(),
Self::Percent80 => "80%".into(),
Self::Percent90 => "90%".into(),
Self::Custom(value) => value.into(),
}
}
}
// **< ItemOrder >**********************************************************************************
/// Posición en [`FlexItem`](super::FlexItem) para un ítem en el orden visual.
///
/// # Accesibilidad
///
/// Con `ItemOrder` se cambia únicamente el **orden visual**, no el orden del documento que siguen
/// la navegación por tabulador y los lectores de pantalla. Al reordenar con `ItemOrder` se puede
/// desalinear lo que se ve en pantalla de lo que se lee o se recorre con teclado, sin ningún aviso
/// del navegador.
///
/// Por eso se recomienda usar únicamente en reordenaciones puramente cosméticas, donde ese
/// desajuste no importe (p. ej. dos bloques intercambiables sin relación de lectura entre sí). Si
/// el orden tiene significado real, cambia el orden en el propio documento en lugar de maquillarlo
/// con `ItemOrder`.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemOrder {
/// Por defecto, el orden visual coincide con el del documento (`order: 0` no explícito).
#[default]
Default,
/// Se muestra antes que cualquier ítem, incluidos los que usan [`Self::Custom`]
/// (`order: -129`).
First,
/// Se muestra después de cualquier ítem, incluidos los que usan [`Self::Custom`]
/// (`order: 128`).
Last,
/// Posición `1` en el orden visual (`order: 1`).
Is1,
/// Posición `2` en el orden visual (`order: 2`).
Is2,
/// Posición `3` en el orden visual (`order: 3`).
Is3,
/// Posición `4` en el orden visual (`order: 4`).
Is4,
/// Posición `5` en el orden visual (`order: 5`).
Is5,
/// Cualquier otra posición no cubierta por `Default` (posición `0`) ni `Is1`..`Is5`.
Custom(i8),
}
impl ItemOrder {
// Devuelve el valor CSS de `order`, o "" para el valor por defecto. `First`/`Last` usan el
// primer entero fuera del rango de `Custom` (`i8::MIN - 1` / `i8::MAX + 1`), para quedar
// siempre antes o después de cualquier valor que éste pueda representar.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::First => "-129".into(),
Self::Last => "128".into(),
Self::Is1 => "1".into(),
Self::Is2 => "2".into(),
Self::Is3 => "3".into(),
Self::Is4 => "4".into(),
Self::Is5 => "5".into(),
Self::Custom(value) => value.to_string().into(),
}
}
}
// **< ItemShrink >*********************************************************************************
/// Factor de reducción en [`FlexItem`](super::FlexItem) para un ítem dentro de un contenedor.
///
/// # Cuándo usar `Is0`
///
/// Para un tamaño fijo ([`ItemSize::Custom`]) es la opción natural. Un icono, un avatar o una barra
/// lateral con un ancho fijo definido por diseño no debe deformarse si falta espacio, que sea otro
/// 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`].
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ItemShrink {
/// Por defecto, puede encoger si hace falta (`flex-shrink: 1` no explícito).
#[default]
Default,
/// No encoge nunca, aunque no quepa en el contenedor (`flex-shrink: 0`).
Is0,
}
impl ItemShrink {
// Devuelve el valor CSS de `flex-shrink`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Is0 => "0".into(),
}
}
}
// **< ItemSize >***********************************************************************************
/// Ancho en [`FlexItem`](super::FlexItem) para un ítem como fracción del contenedor.
///
/// Permite maquetar rejillas de columnas fijas. Un ítem con [`ItemSize::Percent33`] ocupa un tercio
/// del ancho del contenedor con independencia de su contenido.
///
/// # 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.
///
/// Ese sobrante se compensa solo, sin ningún ajuste manual, siempre que:
///
/// - **No se fuerce [`ItemShrink::Is0`]** en los ítems de esa fila. Déjalos en su valor por
/// defecto, [`ItemShrink::Default`](super::ItemShrink::Default). El reparto por defecto del
/// espacio negativo entre elementos, proporcional al tamaño de partida de cada uno, reproduce
/// exactamente el resultado de restar el `gap` antes de repartir. Forzar `ItemShrink::Is0`
/// desactiva esa compensación y el hueco sobrante pasa a desbordar de verdad.
/// - **El contenedor use [`Behavior::NoWrap`](super::Behavior::NoWrap)** (su valor por defecto).
/// Con [`Behavior::Wrap`](super::Behavior::Wrap) el navegador decide si rompe la línea a partir
/// de los tamaños *antes* de aplicar el `shrink`, así que una fila que encajaría perfectamente en
/// una sola línea puede saltar de línea antes de que la compensación llegue a actuar. Combinar
/// `ItemSize` porcentual, `Gap` y ajuste de línea sigue siendo el caso sin resolver porque no hay
/// compensación automática posible cuando distintas líneas acaban con un número distinto de
/// elementos.
///
/// Con un tamaño fijo ([`ItemSize::Custom`]) ninguna de estas condiciones aplica: un `gap` nunca
/// sorprende a un tamaño que no dependía de un porcentaje del contenedor, así que ahí
/// `ItemShrink::Is0` es siempre seguro (ver [`ItemShrink`]).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
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%`).
Percent10,
/// Ocupa el 20% del ancho del contenedor (`flex-basis: 20%`).
Percent20,
/// Ocupa el 25% del ancho del contenedor (`flex-basis: 25%`).
Percent25,
/// Ocupa un tercio del ancho del contenedor (`flex-basis: 33.3333%`).
Percent33,
/// Ocupa el 40% del ancho del contenedor (`flex-basis: 40%`).
Percent40,
/// Ocupa la mitad del ancho del contenedor (`flex-basis: 50%`).
Percent50,
/// Ocupa el 60% del ancho del contenedor (`flex-basis: 60%`).
Percent60,
/// Ocupa dos tercios del ancho del contenedor (`flex-basis: 66.6667%`).
Percent66,
/// Ocupa el 75% del ancho del contenedor (`flex-basis: 75%`).
Percent75,
/// Ocupa el 80% del ancho del contenedor (`flex-basis: 80%`).
Percent80,
/// Ocupa el 90% del ancho del contenedor (`flex-basis: 90%`).
Percent90,
/// Ocupa el 100% del ancho del contenedor (`flex-basis: 100%`).
Percent100,
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un ancho fijo en píxeles).
Custom(UnitValue),
}
impl ItemSize {
// Devuelve el valor CSS de `flex-basis`, o "" para el valor por defecto.
pub(super) fn value(self) -> CowStr {
match self {
Self::Default => "".into(),
Self::Percent10 => "10%".into(),
Self::Percent20 => "20%".into(),
Self::Percent25 => "25%".into(),
Self::Percent33 => "33.3333%".into(),
Self::Percent40 => "40%".into(),
Self::Percent50 => "50%".into(),
Self::Percent60 => "60%".into(),
Self::Percent66 => "66.6667%".into(),
Self::Percent75 => "75%".into(),
Self::Percent80 => "80%".into(),
Self::Percent90 => "90%".into(),
Self::Percent100 => "100%".into(),
Self::Custom(value) => value.into(),
}
}
}

View file

@ -1,4 +1,5 @@
use crate::core::TypeInfo;
use crate::html::flex::FlexItem;
use crate::html::maud::{Escaper, RenderAttrs};
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
@ -89,6 +90,9 @@ pub enum PropsError {
/// estructura de un componente ya definido, temas y extensiones pueden definir un trait con nuevos
/// métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como si
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
///
/// Finalmente, [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem sobre
/// cualquier componente.
#[derive(Clone, Debug)]
pub enum PropsOp {
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
@ -159,6 +163,22 @@ pub enum PropsOp {
SetExtra(&'static str, PropsExtra),
/// Elimina el valor extra asociado a la clave indicada, si existe.
RemoveExtra(&'static str),
/// Aplica un posicionamiento [`FlexItem`] a un componente particular en un contenedor [`Flex`].
/// Añade directamente sus estilos Flexbox al propio componente, sin usar clases CSS.
///
/// 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).
///
/// 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()`]).
///
/// [`Flex`]: crate::html::flex::Flex
/// [`Container::flex()`]: crate::base::component::Container::flex
FlexItem(FlexItem),
}
impl PropsOp {
@ -295,6 +315,11 @@ impl PropsOp {
pub fn remove_extra(key: &'static str) -> Self {
Self::RemoveExtra(key)
}
/// Crea la variante [`FlexItem`](Self::FlexItem) con el posicionamiento indicado.
pub fn flex_item(placement: FlexItem) -> Self {
Self::FlexItem(placement)
}
}
// **< Props >**************************************************************************************
@ -627,6 +652,9 @@ impl Props {
PropsOp::RemoveExtra(key) => {
self.extras.remove(key);
}
PropsOp::FlexItem(placement) => {
placement.apply_to(self);
}
}
self
}

View file

@ -1,4 +1,4 @@
use crate::{AutoDefault, util};
use crate::{AutoDefault, CowStr, util};
use serde::{Deserialize, Deserializer};
@ -42,14 +42,12 @@ use std::str::FromStr;
///
/// ```rust
/// # use pagetop::prelude::*;
/// use std::str::FromStr;
///
/// assert_eq!(UnitValue::from_str("16px").unwrap(), UnitValue::Px(16));
/// assert_eq!(UnitValue::from_str("1.25rem").unwrap(), UnitValue::RelRem(1.25));
/// assert_eq!(UnitValue::from_str("33%").unwrap(), UnitValue::RelPct(33.0));
/// assert_eq!(UnitValue::from_str("auto").unwrap(), UnitValue::Auto);
/// assert_eq!(UnitValue::from_str("").unwrap(), UnitValue::None);
/// assert_eq!(UnitValue::from_str("0").unwrap(), UnitValue::Zero);
/// assert_eq!(Ok(UnitValue::Px(16)), "16px".parse());
/// assert_eq!(Ok(UnitValue::RelRem(1.25)), "1.25rem".parse());
/// assert_eq!(Ok(UnitValue::RelPct(33.0)), "33%".parse());
/// assert_eq!(Ok(UnitValue::Auto), "auto".parse());
/// assert_eq!(Ok(UnitValue::None), "".parse());
/// assert_eq!(Ok(UnitValue::Zero), "0".parse());
/// ```
///
/// # Notas
@ -165,6 +163,14 @@ impl fmt::Display for UnitValue {
}
}
impl From<UnitValue> for CowStr {
/// Delega en `Display`; siempre produce un `Cow::Owned`, porque `to_string()` reserva un
/// `String` nuevo con independencia del contenido.
fn from(value: UnitValue) -> Self {
value.to_string().into()
}
}
/// Convierte una cadena a [`UnitValue`] siguiendo una gramática CSS acotada.
///
/// # Acepta
@ -182,10 +188,8 @@ impl fmt::Display for UnitValue {
///
/// ```rust
/// # use pagetop::prelude::*;
/// use std::str::FromStr;
///
/// assert_eq!(UnitValue::from_str("12px").unwrap(), UnitValue::Px(12));
/// assert!(UnitValue::from_str("12").is_err());
/// assert_eq!("12px".parse(), Ok(UnitValue::Px(12)));
/// assert!("12".parse::<UnitValue>().is_err());
/// ```
///
/// # Errores de interpretación