⚡️ (pagetop): Mejora uso de Props, Grid y HttpRequest
This commit is contained in:
parent
58b4b4e036
commit
5b2ddeb6cc
5 changed files with 228 additions and 38 deletions
|
|
@ -2,6 +2,8 @@ use crate::html::align::{Content, Gap, Items};
|
|||
use crate::html::grid::{AutoFlow, AxisTrack, ContentJustify, DefaultJustify, Tracks};
|
||||
use crate::prelude::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
// **< DisplayGrid >********************************************************************************
|
||||
|
||||
// Posicionamiento CSS Grid del contenedor `Grid`. La API pública sólo expone los constructores
|
||||
|
|
@ -67,12 +69,13 @@ pub struct Grid {
|
|||
// público; `new()`, `at()`, `inline()` e `inline_at()` son la única forma de activarlo.
|
||||
#[getters(skip)]
|
||||
display: Option<DisplayGrid>,
|
||||
/// Devuelve las pistas de columna (`grid-template-columns`), por punto de corte.
|
||||
#[getters(copy)]
|
||||
columns: Responsive<Tracks>,
|
||||
/// Devuelve las pistas de fila (`grid-template-rows`), por punto de corte.
|
||||
#[getters(copy)]
|
||||
rows: Responsive<Tracks>,
|
||||
// Pistas de columna y de fila. Cada `Responsive<Tracks>` ocupa más de 1 KB (doce pistas por
|
||||
// punto de corte), y suma el 80 % del tamaño del componente: se guardan aparte, tras un `Arc`
|
||||
// que sólo se reserva al establecer las primeras. Clonar el `Grid` para renderizarlo sólo suma
|
||||
// una referencia, y `Arc::make_mut` las duplica si se modifican estando compartidas. Sin getter
|
||||
// derivado: `columns()` y `rows()` devuelven el valor por copia, como siempre.
|
||||
#[getters(skip)]
|
||||
templates: Option<Arc<Templates>>,
|
||||
/// Devuelve el tamaño de las columnas generadas implícitamente (`grid-auto-columns`), por
|
||||
/// punto de corte.
|
||||
#[getters(copy)]
|
||||
|
|
@ -103,6 +106,13 @@ pub struct Grid {
|
|||
children: Children,
|
||||
}
|
||||
|
||||
// Pistas de columna y de fila de un `Grid`; ver `Grid::templates`.
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
struct Templates {
|
||||
columns: Responsive<Tracks>,
|
||||
rows: Responsive<Tracks>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Grid {
|
||||
fn new() -> Self {
|
||||
|
|
@ -178,25 +188,29 @@ impl Grid {
|
|||
|
||||
/// Establece las pistas de columna.
|
||||
pub fn with_columns(mut self, tracks: Tracks) -> Self {
|
||||
self.columns = self.columns.set(tracks);
|
||||
let templates = self.templates_mut();
|
||||
templates.columns = templates.columns.set(tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de columna a partir del punto de corte indicado.
|
||||
pub fn with_columns_at(mut self, bp: Breakpoint, tracks: Tracks) -> Self {
|
||||
self.columns = self.columns.set_at(bp, tracks);
|
||||
let templates = self.templates_mut();
|
||||
templates.columns = templates.columns.set_at(bp, tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de fila.
|
||||
pub fn with_rows(mut self, tracks: Tracks) -> Self {
|
||||
self.rows = self.rows.set(tracks);
|
||||
let templates = self.templates_mut();
|
||||
templates.rows = templates.rows.set(tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de fila a partir del punto de corte indicado.
|
||||
pub fn with_rows_at(mut self, bp: Breakpoint, tracks: Tracks) -> Self {
|
||||
self.rows = self.rows.set_at(bp, tracks);
|
||||
let templates = self.templates_mut();
|
||||
templates.rows = templates.rows.set_at(bp, tracks);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +330,30 @@ impl Grid {
|
|||
}
|
||||
|
||||
impl Grid {
|
||||
// **< Grid GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las pistas de columna (`grid-template-columns`), por punto de corte.
|
||||
pub fn columns(&self) -> Responsive<Tracks> {
|
||||
self.templates
|
||||
.as_deref()
|
||||
.map_or_else(Default::default, |t| t.columns)
|
||||
}
|
||||
|
||||
/// Devuelve las pistas de fila (`grid-template-rows`), por punto de corte.
|
||||
pub fn rows(&self) -> Responsive<Tracks> {
|
||||
self.templates
|
||||
.as_deref()
|
||||
.map_or_else(Default::default, |t| t.rows)
|
||||
}
|
||||
|
||||
// **< Grid PRIVATE >***************************************************************************
|
||||
|
||||
// Pistas para modificarlas: las crea con sus valores por defecto si aún no existen y, si están
|
||||
// compartidas con un clon, las duplica antes.
|
||||
fn templates_mut(&mut self) -> &mut Templates {
|
||||
Arc::make_mut(self.templates.get_or_insert_with(Default::default))
|
||||
}
|
||||
|
||||
// Calcula las clases CSS *responsive* de este contenedor Grid y las añade a `classes`,
|
||||
// separadas con un espacio de las que ya hubiera, para poder compartir un único acumulador con
|
||||
// `GridItem::apply()` (ver `Props::unpack_with_classes()`) sin cadenas intermedias.
|
||||
|
|
@ -347,8 +385,9 @@ impl Grid {
|
|||
};
|
||||
styles(cx, classes, entry, class, "display", value.into());
|
||||
|
||||
apply!(cx, classes, self.columns, "_grid-columns_", "grid-template-columns", val);
|
||||
apply!(cx, classes, self.rows, "_grid-rows_", "grid-template-rows", val);
|
||||
let (columns, rows) = (self.columns(), self.rows());
|
||||
apply!(cx, classes, columns, "_grid-columns_", "grid-template-columns", val);
|
||||
apply!(cx, classes, rows, "_grid-rows_", "grid-template-rows", val);
|
||||
apply!(cx, classes, self.auto_columns, "_grid-auto-columns_", "grid-auto-columns", val);
|
||||
apply!(cx, classes, self.auto_rows, "_grid-auto-rows_", "grid-auto-rows", val);
|
||||
// `val`: a diferencia de `flex::Behavior` (una sola palabra en cualquier variante),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
|||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::panic::Location;
|
||||
use std::sync::Arc;
|
||||
|
||||
// **< Props >**************************************************************************************
|
||||
|
||||
|
|
@ -191,6 +192,17 @@ pub struct Props {
|
|||
styles: Vec<(CowStr, CowStr)>,
|
||||
attrs: Vec<(CowStr, CowStr)>,
|
||||
extras: HashMap<&'static str, PropsExtra>,
|
||||
// Posicionamiento Flexbox/Grid y espaciado. Cada uno lleva un valor por punto de corte, lo que
|
||||
// suma más de 1 KB, y casi ningún componente los usa. Se guardan aparte, tras un `Arc` que sólo
|
||||
// se reserva al establecer el primero. Así `Props` (y con él cada componente, el `Context` y la
|
||||
// `Page`) es diez veces menor, y clonar el componente para renderizarlo no copia esos valores,
|
||||
// sólo cuenta una referencia (`Arc::make_mut` los duplica si se modifican estando compartidos).
|
||||
layout: Option<Arc<Layout>>,
|
||||
}
|
||||
|
||||
// Posicionamiento y espaciado de un componente; ver `Props::layout`.
|
||||
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
||||
struct Layout {
|
||||
flex_item: FlexItem,
|
||||
grid_item: GridItem,
|
||||
margin: Margin,
|
||||
|
|
@ -344,16 +356,20 @@ impl Props {
|
|||
self.extras.remove(key);
|
||||
}
|
||||
PropsOp::FlexItem(placement) => {
|
||||
self.flex_item = self.flex_item.merge(placement);
|
||||
let layout = self.layout_mut();
|
||||
layout.flex_item = layout.flex_item.merge(placement);
|
||||
}
|
||||
PropsOp::GridItem(placement) => {
|
||||
self.grid_item = self.grid_item.merge(placement);
|
||||
let layout = self.layout_mut();
|
||||
layout.grid_item = layout.grid_item.merge(placement);
|
||||
}
|
||||
PropsOp::Margin(margin) => {
|
||||
self.margin = self.margin.merge(margin);
|
||||
let layout = self.layout_mut();
|
||||
layout.margin = layout.margin.merge(margin);
|
||||
}
|
||||
PropsOp::Padding(padding) => {
|
||||
self.padding = self.padding.merge(padding);
|
||||
let layout = self.layout_mut();
|
||||
layout.padding = layout.padding.merge(padding);
|
||||
}
|
||||
}
|
||||
self
|
||||
|
|
@ -453,10 +469,10 @@ impl Props {
|
|||
&& 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()
|
||||
&& self
|
||||
.layout
|
||||
.as_deref()
|
||||
.is_none_or(|layout| *layout == Layout::default())
|
||||
}
|
||||
|
||||
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
||||
|
|
@ -619,16 +635,24 @@ impl Props {
|
|||
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);
|
||||
if let Some(layout) = self.layout.as_deref() {
|
||||
layout.flex_item.apply(cx, &mut classes);
|
||||
layout.grid_item.apply(cx, &mut classes);
|
||||
layout.margin.apply(cx, &mut classes);
|
||||
layout.padding.apply(cx, &mut classes);
|
||||
}
|
||||
PropsUnpack {
|
||||
props: self,
|
||||
classes,
|
||||
}
|
||||
}
|
||||
|
||||
// Posicionamiento y espaciado para modificarlos: los crea con sus valores por defecto si aún no
|
||||
// existen y, si están compartidos con un clon, los duplica antes.
|
||||
fn layout_mut(&mut self) -> &mut Layout {
|
||||
Arc::make_mut(self.layout.get_or_insert_with(Default::default))
|
||||
}
|
||||
|
||||
fn apply_id(&mut self, id: &str) {
|
||||
self.id = util::normalize_token(id);
|
||||
}
|
||||
|
|
|
|||
35
src/web.rs
35
src/web.rs
|
|
@ -73,16 +73,23 @@ use std::task::{Context, Poll};
|
|||
/// impone Axum: un extractor que consuma el cuerpo de la petición (`Form<T>`, `RawForm`, etc.) debe
|
||||
/// ir siempre el último.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpRequest {
|
||||
pub struct HttpRequest(Arc<RequestParts>);
|
||||
|
||||
// Datos de la petición. Se guardan tras un `Arc` para que `HttpRequest` ocupe un puntero y clonarlo
|
||||
// sea una simple cuenta de referencias: viaja dentro de `ErrorPage` y de los `Result<_, ErrorPage>`
|
||||
// de los handlers, donde un tipo grande se copiaría en cada retorno.
|
||||
#[derive(Debug)]
|
||||
struct RequestParts {
|
||||
uri: http::Uri,
|
||||
headers: http::HeaderMap,
|
||||
extensions: Arc<http::Extensions>,
|
||||
extensions: http::Extensions,
|
||||
}
|
||||
|
||||
impl HttpRequest {
|
||||
/// Devuelve la URI completa de la petición, incluyendo la *query string* si la hay.
|
||||
pub fn uri(&self) -> &str {
|
||||
self.uri
|
||||
self.0
|
||||
.uri
|
||||
.path_and_query()
|
||||
.map(|pq| pq.as_str())
|
||||
.unwrap_or("/")
|
||||
|
|
@ -90,19 +97,19 @@ impl HttpRequest {
|
|||
|
||||
/// Devuelve la ruta (*path*) de la petición, sin la *query string*.
|
||||
pub fn path(&self) -> &str {
|
||||
self.uri.path()
|
||||
self.0.uri.path()
|
||||
}
|
||||
|
||||
/// Devuelve la cadena de consulta (*query string*) de la petición, sin el carácter `?`.
|
||||
///
|
||||
/// Devuelve una cadena vacía si la petición no tiene *query string*.
|
||||
pub fn query_string(&self) -> &str {
|
||||
self.uri.query().unwrap_or("")
|
||||
self.0.uri.query().unwrap_or("")
|
||||
}
|
||||
|
||||
/// Devuelve las cabeceras HTTP de la petición.
|
||||
pub fn headers(&self) -> &http::HeaderMap {
|
||||
&self.headers
|
||||
&self.0.headers
|
||||
}
|
||||
|
||||
/// Accede a un valor inyectado por middleware en las extensiones de la petición.
|
||||
|
|
@ -112,7 +119,7 @@ impl HttpRequest {
|
|||
///
|
||||
/// El tipo debe implementar `Send + Sync + 'static` (requisito de [`http::Extensions`]).
|
||||
pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
|
||||
self.extensions.get::<T>()
|
||||
self.0.extensions.get::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,17 +128,17 @@ impl<S: Send + Sync> FromRequestParts<S> for HttpRequest {
|
|||
|
||||
// Clona (no toma) las extensiones inyectadas por middleware, para que otros extractores del
|
||||
// handler (`Path<T>`, `Extension<T>`, etc.) las sigan viendo intactas sin importar en qué
|
||||
// posición se declare `HttpRequest`. El clon se envuelve en un `Arc` compartido para que
|
||||
// posición se declare `HttpRequest`. Todo se guarda tras un `Arc` compartido para que
|
||||
// `HttpRequest` sea `Clone` a coste mínimo en el resto de su ciclo de vida.
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
Ok(HttpRequest {
|
||||
Ok(HttpRequest(Arc::new(RequestParts {
|
||||
uri: parts.uri.clone(),
|
||||
headers: parts.headers.clone(),
|
||||
extensions: Arc::new(parts.extensions.clone()),
|
||||
})
|
||||
extensions: parts.extensions.clone(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -417,11 +424,11 @@ pub mod test {
|
|||
/// [`Context::new`](crate::core::component::Context::new) en tests unitarios de componentes.
|
||||
pub fn to_http_request(self) -> super::HttpRequest {
|
||||
let uri = self.uri.parse().unwrap();
|
||||
super::HttpRequest {
|
||||
super::HttpRequest(std::sync::Arc::new(super::RequestParts {
|
||||
uri,
|
||||
headers: self.headers,
|
||||
extensions: std::sync::Arc::new(self.extensions),
|
||||
}
|
||||
extensions: self.extensions,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
57
tests/component_grid_layout.rs
Normal file
57
tests/component_grid_layout.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// Las pistas de columna y de fila se guardan aparte para que `Grid` sea pequeño; esta prueba
|
||||
// avisa si algo vuelve a inflarlo.
|
||||
#[pagetop::test]
|
||||
async fn grid_stays_small() {
|
||||
assert!(
|
||||
std::mem::size_of::<Grid>() <= 1024,
|
||||
"Grid ocupa {} bytes",
|
||||
std::mem::size_of::<Grid>()
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn getters_return_the_default_until_tracks_are_set() {
|
||||
let grid = Grid::new();
|
||||
assert_eq!(grid.columns(), Responsive::default());
|
||||
assert_eq!(grid.rows(), Responsive::default());
|
||||
|
||||
let tracks = grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0));
|
||||
let grid = grid.with_columns(tracks);
|
||||
assert_eq!(grid.columns(), Responsive::default().set(tracks));
|
||||
// Las filas siguen sin definir.
|
||||
assert_eq!(grid.rows(), Responsive::default());
|
||||
}
|
||||
|
||||
// Un clon comparte las pistas hasta que se modifica; entonces cada uno tiene las suyas.
|
||||
#[pagetop::test]
|
||||
async fn a_clone_does_not_share_later_changes() {
|
||||
let three = grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0));
|
||||
let two = grid::Tracks::repeat(2, grid::AxisTrack::Fraction(1.0));
|
||||
let original = Grid::new().with_columns(three);
|
||||
let copy = original.clone().with_columns(two).with_rows(two);
|
||||
|
||||
assert_eq!(original.columns(), Responsive::default().set(three));
|
||||
assert_eq!(original.rows(), Responsive::default());
|
||||
assert_eq!(copy.columns(), Responsive::default().set(two));
|
||||
assert_eq!(copy.rows(), Responsive::default().set(two));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn columns_and_rows_by_breakpoint_are_kept() {
|
||||
let one = grid::Tracks::repeat(1, grid::AxisTrack::Fraction(1.0));
|
||||
let four = grid::Tracks::repeat(4, grid::AxisTrack::Fraction(1.0));
|
||||
let grid = Grid::new()
|
||||
.with_columns(one)
|
||||
.with_columns_at(Breakpoint::Lg, four)
|
||||
.with_rows_at(Breakpoint::Md, one);
|
||||
assert_eq!(
|
||||
grid.columns(),
|
||||
Responsive::default().set(one).set_at(Breakpoint::Lg, four)
|
||||
);
|
||||
assert_eq!(
|
||||
grid.rows(),
|
||||
Responsive::default().set_at(Breakpoint::Md, one)
|
||||
);
|
||||
}
|
||||
63
tests/html_props_layout.rs
Normal file
63
tests/html_props_layout.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// El posicionamiento y el espaciado se guardan aparte para que `Props` (y con él cada componente)
|
||||
// sea pequeño; esta prueba avisa si algo vuelve a inflarlo.
|
||||
#[pagetop::test]
|
||||
async fn props_stays_small() {
|
||||
assert!(
|
||||
std::mem::size_of::<Props>() <= 192,
|
||||
"Props ocupa {} bytes",
|
||||
std::mem::size_of::<Props>()
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn is_empty_tracks_layout_values() {
|
||||
let props = Props::default();
|
||||
assert!(props.is_empty());
|
||||
|
||||
let with_margin = props
|
||||
.clone()
|
||||
.with_prop(Margin::new().with_top(UnitValue::Zero));
|
||||
assert!(!with_margin.is_empty());
|
||||
let with_padding = Props::default().with_prop(Padding::new().with_x(UnitValue::Px(4)));
|
||||
assert!(!with_padding.is_empty());
|
||||
|
||||
// Un valor por defecto no cuenta como definido.
|
||||
assert!(Props::default().with_prop(Margin::new()).is_empty());
|
||||
assert!(Props::default().with_prop(Padding::new()).is_empty());
|
||||
}
|
||||
|
||||
// Un clon comparte el posicionamiento hasta que se modifica; entonces cada uno tiene el suyo.
|
||||
#[pagetop::test]
|
||||
async fn a_clone_does_not_share_later_changes() {
|
||||
let original = Props::default().with_prop(Margin::new().with_top(UnitValue::RelRem(1.0)));
|
||||
let mut copy = original.clone();
|
||||
copy.alter_prop(Margin::new().with_top(UnitValue::RelRem(2.0)));
|
||||
copy.alter_prop(Padding::new().with_start(UnitValue::Px(3)));
|
||||
|
||||
let mut cx = Context::default();
|
||||
let html_original = html! { span (original.unpack(&mut cx)) {} }.into_string();
|
||||
let html_copy = html! { span (copy.unpack(&mut cx)) {} }.into_string();
|
||||
|
||||
assert!(
|
||||
html_original.contains("_margin-top_1rem_"),
|
||||
"{html_original}"
|
||||
);
|
||||
assert!(!html_original.contains("padding"), "{html_original}");
|
||||
assert!(html_copy.contains("_margin-top_2rem_"), "{html_copy}");
|
||||
assert!(html_copy.contains("_padding-start_3px_"), "{html_copy}");
|
||||
}
|
||||
|
||||
// Todos los tipos de posicionamiento conviven y se emiten en el mismo orden de siempre.
|
||||
#[pagetop::test]
|
||||
async fn layout_kinds_combine_in_the_usual_order() {
|
||||
let props = Props::default()
|
||||
.with_prop(Padding::new().with_top(UnitValue::Zero))
|
||||
.with_prop(Margin::new().with_top(UnitValue::Zero));
|
||||
let mut cx = Context::default();
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let margin = html.find("_margin-top_").expect("margin");
|
||||
let padding = html.find("_padding-top_").expect("padding");
|
||||
assert!(margin < padding, "{html}");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue