From 39f494539e52af687c237b79d110021ea7e08f9e Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 26 Jul 2026 10:00:43 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(htmx):=20Expone=20req?= =?UTF-8?q?uest=20y=20response=20como=20p=C3=BAblicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-htmx/src/hx.rs | 30 +++++++++++++----------- extensions/pagetop-htmx/src/lib.rs | 9 ++----- extensions/pagetop-htmx/src/request.rs | 3 +-- extensions/pagetop-htmx/src/response.rs | 5 ++-- extensions/pagetop-htmx/tests/request.rs | 2 +- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/extensions/pagetop-htmx/src/hx.rs b/extensions/pagetop-htmx/src/hx.rs index 2f1edc34..fb4ae54b 100644 --- a/extensions/pagetop-htmx/src/hx.rs +++ b/extensions/pagetop-htmx/src/hx.rs @@ -381,7 +381,8 @@ pub fn on_htmx(event: &str) -> String { /// /// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con /// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores -/// directamente, aunque lo habitual es usar el trait [`HtmxRequestExt`](crate::HtmxRequestExt). +/// directamente, aunque lo habitual es usar el trait +/// [`HtmxRequestExt`](crate::request::HtmxRequestExt). /// /// ```rust,no_run /// use pagetop::prelude::*; @@ -417,7 +418,8 @@ pub mod request { /// Cabeceras de respuesta HTTP para HTMX. /// /// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas -/// manualmente, aunque lo habitual es usar el constructor [`HtmxResponse`](crate::HtmxResponse). +/// manualmente, aunque lo habitual es usar el constructor +/// [`HtmxResponse`](crate::response::HtmxResponse). /// /// ```rust,no_run /// use pagetop::prelude::*; @@ -431,38 +433,38 @@ pub mod request { /// ``` pub mod response { /// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver - /// [`HtmxResponse::location()`](crate::HtmxResponse::location) y - /// [`HtmxResponse::location_json()`](crate::HtmxResponse::location_json). + /// [`HtmxResponse::location()`](crate::response::HtmxResponse::location) y + /// [`HtmxResponse::location_json()`](crate::response::HtmxResponse::location_json). pub const LOCATION: &str = "HX-Location"; /// Empuja la URL indicada al historial del navegador. Ver - /// [`HtmxResponse::push_url()`](crate::HtmxResponse::push_url). + /// [`HtmxResponse::push_url()`](crate::response::HtmxResponse::push_url). pub const PUSH_URL: &str = "HX-Push-Url"; /// Provoca una redirección completa del navegador. Ver - /// [`HtmxResponse::redirect()`](crate::HtmxResponse::redirect). + /// [`HtmxResponse::redirect()`](crate::response::HtmxResponse::redirect). pub const REDIRECT: &str = "HX-Redirect"; /// Provoca una recarga completa de la página. Ver - /// [`HtmxResponse::refresh()`](crate::HtmxResponse::refresh). + /// [`HtmxResponse::refresh()`](crate::response::HtmxResponse::refresh). pub const REFRESH: &str = "HX-Refresh"; /// Reemplaza la URL actual en el historial. Ver - /// [`HtmxResponse::replace_url()`](crate::HtmxResponse::replace_url). + /// [`HtmxResponse::replace_url()`](crate::response::HtmxResponse::replace_url). pub const REPLACE_URL: &str = "HX-Replace-Url"; /// Anula el `hx-swap` del elemento. Ver - /// [`HtmxResponse::reswap()`](crate::HtmxResponse::reswap). + /// [`HtmxResponse::reswap()`](crate::response::HtmxResponse::reswap). pub const RESWAP: &str = "HX-Reswap"; /// Anula el `hx-target` del elemento. Ver - /// [`HtmxResponse::retarget()`](crate::HtmxResponse::retarget). + /// [`HtmxResponse::retarget()`](crate::response::HtmxResponse::retarget). pub const RETARGET: &str = "HX-Retarget"; /// Anula el `hx-select` del elemento. Ver - /// [`HtmxResponse::reselect()`](crate::HtmxResponse::reselect). + /// [`HtmxResponse::reselect()`](crate::response::HtmxResponse::reselect). pub const RESELECT: &str = "HX-Reselect"; /// Dispara eventos JavaScript al completar la respuesta. Ver - /// [`HtmxResponse::trigger()`](crate::HtmxResponse::trigger). + /// [`HtmxResponse::trigger()`](crate::response::HtmxResponse::trigger). pub const TRIGGER: &str = "HX-Trigger"; /// Dispara eventos tras la fase *settle*. Ver - /// [`HtmxResponse::trigger_after_settle()`](crate::HtmxResponse::trigger_after_settle). + /// [`HtmxResponse::trigger_after_settle()`](crate::response::HtmxResponse::trigger_after_settle). pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle"; /// Dispara eventos tras el *swap*. Ver - /// [`HtmxResponse::trigger_after_swap()`](crate::HtmxResponse::trigger_after_swap). + /// [`HtmxResponse::trigger_after_swap()`](crate::response::HtmxResponse::trigger_after_swap). pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap"; } diff --git a/extensions/pagetop-htmx/src/lib.rs b/extensions/pagetop-htmx/src/lib.rs index 30c96dba..f127c227 100644 --- a/extensions/pagetop-htmx/src/lib.rs +++ b/extensions/pagetop-htmx/src/lib.rs @@ -93,18 +93,13 @@ include_locales!(LOCALES_HTMX); pub mod hx; pub mod hx_table; - -mod request; -pub use request::HtmxRequestExt; - -mod response; -pub use response::HtmxResponse; +pub mod request; +pub mod response; /// Prelude de `pagetop-htmx`. pub mod prelude { pub use crate::hx; pub use crate::hx_table; - pub use crate::request::HtmxRequestExt; pub use crate::response::HtmxResponse; } diff --git a/extensions/pagetop-htmx/src/request.rs b/extensions/pagetop-htmx/src/request.rs index ff14e2f0..cb571e71 100644 --- a/extensions/pagetop-htmx/src/request.rs +++ b/extensions/pagetop-htmx/src/request.rs @@ -4,8 +4,7 @@ use pagetop::prelude::*; // **< HtmxRequestExt >***************************************************************************** -/// Extiende [`HttpRequest`](pagetop::web::HttpRequest) con métodos para detectar y leer peticiones -/// HTMX. +/// Extiende [`HttpRequest`] con métodos para detectar y leer peticiones HTMX. /// /// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de /// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente. diff --git a/extensions/pagetop-htmx/src/response.rs b/extensions/pagetop-htmx/src/response.rs index b9221e92..0712ae20 100644 --- a/extensions/pagetop-htmx/src/response.rs +++ b/extensions/pagetop-htmx/src/response.rs @@ -1,4 +1,4 @@ -//! Implementación de [`HtmxResponse`] e [`IntoResponse`](pagetop::web::IntoResponse) para HTMX. +//! Implementación de [`HtmxResponse`] e [`IntoResponse`] para HTMX. use pagetop::prelude::*; @@ -10,8 +10,7 @@ use pagetop::prelude::*; /// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la /// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc. /// -/// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse -/// directamente desde cualquier handler. +/// Implementa [`IntoResponse`], por lo que puede devolverse directamente desde cualquier handler. /// /// # Ejemplo /// diff --git a/extensions/pagetop-htmx/tests/request.rs b/extensions/pagetop-htmx/tests/request.rs index 63bd842c..90995ec0 100644 --- a/extensions/pagetop-htmx/tests/request.rs +++ b/extensions/pagetop-htmx/tests/request.rs @@ -1,5 +1,5 @@ use pagetop::prelude::*; -use pagetop_htmx::HtmxRequestExt; +use pagetop_htmx::prelude::*; struct TestApp; From 65c8a788c20e2f41416893f2fc0764613a0bd09f Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 26 Jul 2026 10:03:54 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20(web):=20A=C3=B1ade=20cabeceras?= =?UTF-8?q?=20HTTP=20a=20TestRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index 4dd79b77..498f9570 100644 --- a/src/web.rs +++ b/src/web.rs @@ -347,6 +347,7 @@ pub mod test { pub struct TestRequest { method: http::Method, uri: String, + headers: http::HeaderMap, extensions: http::Extensions, } @@ -356,6 +357,7 @@ pub mod test { Self { method: http::Method::GET, uri: "/".to_owned(), + headers: http::HeaderMap::new(), extensions: http::Extensions::new(), } } @@ -365,6 +367,7 @@ pub mod test { Self { method: http::Method::POST, uri: "/".to_owned(), + headers: http::HeaderMap::new(), extensions: http::Extensions::new(), } } @@ -375,6 +378,20 @@ pub mod test { self } + /// Añade una cabecera a la petición. + /// + /// Si `name` o `value` no son válidos como cabecera HTTP, se descarta en silencio en lugar + /// de entrar en pánico. + pub fn header(mut self, name: impl AsRef, value: impl AsRef) -> Self { + if let (Ok(n), Ok(v)) = ( + http::HeaderName::from_bytes(name.as_ref().as_bytes()), + http::HeaderValue::from_str(value.as_ref()), + ) { + self.headers.insert(n, v); + } + self + } + /// Inserta un valor en las extensiones de la petición. /// /// Útil para simular lo que un middleware haría en producción antes de que el handler @@ -391,6 +408,7 @@ pub mod test { .uri(self.uri) .body(Body::empty()) .unwrap(); + *req.headers_mut() = self.headers; *req.extensions_mut() = self.extensions; req } @@ -401,7 +419,7 @@ pub mod test { let uri = self.uri.parse().unwrap(); super::HttpRequest { uri, - headers: axum::http::HeaderMap::new(), + headers: self.headers, extensions: std::sync::Arc::new(self.extensions), } } From ce6b4d2581f0ff70840fa7caa248c48f15d5096b Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 26 Jul 2026 11:30:16 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(core):=20Simplifica?= =?UTF-8?q?=20Child=20a=20Arc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elimina el `RwLock>` interno ya que ningún punto del ciclo de renderizado tomaba el write-lock. De paso, `From> for Child` pasa a una coerción de puntero sin clonar, gracias al copy-on-write que ya ofrece `Embed::get_mut()`. Los doctests en `rust,ignore` pasan a `rust,no_run`, verificados por el compilador. --- src/core/component/children.rs | 72 +++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/core/component/children.rs b/src/core/component/children.rs index cadcb05c..b76a6258 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -2,8 +2,6 @@ use crate::core::component::{Component, Context}; use crate::html::{Markup, html}; use crate::{AutoDefault, UniqueId, builder_fn}; -use parking_lot::RwLock; - use std::fmt; use std::sync::Arc; use std::vec::IntoIter; @@ -11,14 +9,19 @@ use std::vec::IntoIter; // **< Child >************************************************************************************** /// Representa un componente hijo encapsulado para su uso en una lista [`Children`]. +/// +/// Envuelve el componente en `Arc`, compartido y de sólo lectura. Clonar un `Child` +/// sólo incrementa el contador de referencias. Para renderizar obtiene una copia propia con +/// [`ComponentClone::clone_box()`](crate::core::component::ComponentClone::clone_box), de modo que +/// el componente original nunca se modifica. #[derive(AutoDefault, Clone)] -pub struct Child(Option>>>); +pub struct Child(Option>); impl fmt::Debug for Child { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { None => write!(f, "Child(None)"), - Some(c) => write!(f, "Child({})", c.read().name()), + Some(c) => write!(f, "Child({})", c.name()), } } } @@ -26,7 +29,7 @@ impl fmt::Debug for Child { impl Child { /// Crea un nuevo `Child` a partir de un componente. pub fn with(component: impl Component) -> Self { - Child(Some(Arc::new(RwLock::new(Box::new(component))))) + Child(Some(Arc::new(component))) } // **< Child BUILDER >************************************************************************** @@ -36,7 +39,7 @@ impl Child { /// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia. #[builder_fn] pub fn with_component(mut self, component: Option) -> Self { - self.0 = component.map(|c| Arc::new(RwLock::new(Box::new(c) as Box))); + self.0 = component.map(|c| Arc::new(c) as Arc); self } @@ -45,7 +48,7 @@ impl Child { /// Devuelve el identificador del componente, si existe y está definido. #[inline] pub fn id(&self) -> Option { - self.0.as_ref().and_then(|c| c.read().id()) + self.0.as_ref().and_then(|c| c.id()) } // **< Child RENDER >*************************************************************************** @@ -55,7 +58,7 @@ impl Child { match &self.0 { None => html! {}, Some(m) => { - let mut component = m.read().clone_box(); + let mut component = m.clone_box(); component.render(cx).await } } @@ -66,32 +69,32 @@ impl Child { // Devuelve el [`UniqueId`] del tipo del componente, si el Child no está vacío. #[inline] fn type_id(&self) -> Option { - self.0.as_ref().map(|c| c.read().type_id()) + self.0.as_ref().map(|c| c.type_id()) } } impl From> for Child { /// Convierte un [`Embed`] en un [`Child`], consumiendo el componente tipado. /// - /// Útil cuando se tiene un [`Embed`] y se necesita añadirlo a una lista [`Children`]: + /// Útil cuando se tiene un [`Embed`] para añadir a una lista [`Children`]: /// - /// ```rust,ignore - /// children.with_child(Child::from(my_embed)); - /// // o equivalentemente: - /// children.with_child(my_embed.into()); + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// let my_embed = Embed::with(Html::with(|_| html! { "Text" })); + /// let children = Children::new().with_child(Child::from(my_embed)); + /// + /// // De forma equivalente se puede usar la conversión implícita hacia `Child`: + /// let my_embed = Embed::with(Html::with(|_| html! { "Text" })); + /// let child: Child = my_embed.into(); + /// let children = children.with_child(child); /// ``` fn from(embed: Embed) -> Self { - match embed.0 { - None => Child(None), - Some(arc) => Child(Some(Arc::new(RwLock::new(match Arc::try_unwrap(arc) { - Ok(c) => Box::new(c) as Box, - Err(arc) => arc.clone_box(), - })))), - } + Child(embed.0.map(|arc| arc as Arc)) } } impl From for Child { + /// Convierte cualquier componente en un [`Child`], equivalente a [`Child::with()`]. #[inline] fn from(component: T) -> Self { Child::with(component) @@ -270,12 +273,13 @@ pub enum ChildOp { /// Gracias a esto, [`with_child`](Self::with_child) acepta un componente directamente o cualquier /// variante de [`ChildOp`]: /// -/// ```rust,ignore +/// ```rust,no_run +/// # use pagetop::prelude::*; /// // Añadir al final de la lista (implícito): -/// children.with_child(MiComponente::new()); +/// let children = Children::new().with_child(Html::new()); /// /// // Operación explícita: -/// children.with_child(ChildOp::Prepend(MiComponente::new().into())); +/// let children = children.with_child(ChildOp::Prepend(Html::new().into())); /// ``` #[derive(AutoDefault, Clone, Debug)] pub struct Children(Vec); @@ -458,8 +462,9 @@ impl IntoIterator for Children { /// /// # Ejemplo /// - /// ```rust,ignore - /// let children = Children::new().with_child(child1).with_child(child2); + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// let children = Children::new().with_child(Html::new()).with_child(Html::new()); /// for child in children { /// println!("{:?}", child.id()); /// } @@ -477,8 +482,9 @@ impl<'a> IntoIterator for &'a Children { /// /// # Ejemplo /// - /// ```rust,ignore - /// let children = Children::new().with_child(child1).with_child(child2); + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// let children = Children::new().with_child(Html::new()).with_child(Html::new()); /// for child in &children { /// println!("{:?}", child.id()); /// } @@ -496,10 +502,12 @@ impl<'a> IntoIterator for &'a mut Children { /// /// # Ejemplo /// - /// ```rust,ignore - /// let mut children = Children::new().with_child(child1).with_child(child2); - /// for child in &mut children { - /// child.render(&mut context).await; + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// async fn render_all(mut children: Children, context: &mut Context) { + /// for child in &mut children { + /// child.render(context).await; + /// } /// } /// ``` fn into_iter(self) -> Self::IntoIter { From 845d91d8c15550b9a73ff4f9b8c9c8236a213e6b Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 26 Jul 2026 12:39:01 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(core):=20Evita=20dobl?= =?UTF-8?q?e=20clonado=20de=20InRegion=20en=20Child?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los prototipos registrados con `InRegion` se clonaban dos veces por petición: una al ensamblar la región (`ComponentGlobal::as_child`) y otra al renderizar (`Child::render`). Ahora sólo se clonan al renderizar. --- src/core/component/children.rs | 13 ++++++-- src/core/theme/regions.rs | 60 +++++++++++++--------------------- 2 files changed, 33 insertions(+), 40 deletions(-) diff --git a/src/core/component/children.rs b/src/core/component/children.rs index b76a6258..519c2f2f 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -32,6 +32,13 @@ impl Child { Child(Some(Arc::new(component))) } + // Envuelve un `Arc` ya construido, sin clonar el componente. La usa `core::theme::regions` para + // registrar los prototipos de `InRegion` sin clonar su estado hasta que `render()` obtenga la + // copia propia que necesita mutar. + pub(crate) fn from_arc(component: Arc) -> Self { + Child(Some(component)) + } + // **< Child BUILDER >************************************************************************** /// Establece un componente nuevo, o lo vacía. @@ -73,7 +80,7 @@ impl Child { } } -impl From> for Child { +impl From> for Child { /// Convierte un [`Embed`] en un [`Child`], consumiendo el componente tipado. /// /// Útil cuando se tiene un [`Embed`] para añadir a una lista [`Children`]: @@ -93,7 +100,7 @@ impl From> for Child { } } -impl From for Child { +impl From for Child { /// Convierte cualquier componente en un [`Child`], equivalente a [`Child::with()`]. #[inline] fn from(component: T) -> Self { @@ -101,7 +108,7 @@ impl From for Child { } } -impl From for ChildOp { +impl From for ChildOp { /// Convierte un componente en [`ChildOp::Add`], permitiendo pasar componentes directamente a /// métodos como [`Children::with_child`] sin envolverlos explícitamente. #[inline] diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index f6c00aed..515337f3 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -7,26 +7,13 @@ use parking_lot::RwLock; use std::collections::HashMap; use std::sync::{Arc, LazyLock}; -// Permite almacenar un componente como prototipo en regiones globales. +// Mapea cada nombre de región con su lista de prototipos de componentes. // -// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. En cada -// llamada a [`as_child`](Self::as_child) produce un clon fresco del estado original, de modo que -// cada página renderiza el componente desde su estado inicial sin acumular mutaciones de peticiones -// anteriores. -trait ComponentGlobal: Send + Sync { - // Devuelve un nuevo [`Child`] con una copia independiente del componente original. - fn as_child(&self) -> Child; -} - -impl ComponentGlobal for T { - #[inline] - fn as_child(&self) -> Child { - Child::with(self.clone()) - } -} - -// Mapa de nombre de región a lista de prototipos de componentes. -type RegionComponents = HashMap>>; +// Se comparten como `Arc`. El prototipo no se clona al registrarse ni al ensamblar +// la región (sólo se clona el `Arc`, barato). En cambio, sí se realiza un clonado del componente en +// `Child::render()`, cuando cada petición necesita su propia copia mutable para pasar por `setup()` +// desde un estado inicial limpio. +type RegionComponents = HashMap>>; // Regiones globales con prototipos asociados a un tema específico. static THEME_REGIONS: LazyLock>> = @@ -64,23 +51,22 @@ impl ChildrenInRegions { /// /// Se recogen desde tres fuentes disponibles, en el siguiente orden: /// - /// 1. Prototipos globales comunes, disponibles en cualquier tema. Se clonan en cada petición + /// 1. Prototipos globales comunes, disponibles en cualquier tema. Se comparten como `Arc` (sin + /// clonar el componente); `Child::render()` obtiene su propia copia mutable más adelante, /// para que `setup()` parta siempre de un estado inicial limpio. /// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en /// lugar de clonarse, ya que son de un único uso. - /// 3. Prototipos del tema activo, exclusivos del tema en curso. También se clonan para asegurar - /// que llegan a `setup()` con el mismo estado inicial. + /// 3. Prototipos del tema activo, exclusivos del tema en curso. Se comparten igual que los + /// comunes. pub fn assemble_region(&mut self, theme: ThemeRef, region: RegionRef) -> Children { - let region_name = region.name(); - let common = COMMON_REGIONS.read(); - let themed = THEME_REGIONS.read(); - let mut result = Children::new(); + let region_name = region.name(); + // 1. Prototipos globales comunes. - if let Some(protos) = common.get(region_name) { - for proto in protos { - result.add(proto.as_child()); + if let Some(global_protos) = COMMON_REGIONS.read().get(region_name) { + for proto in global_protos { + result.add(Child::from_arc(Arc::clone(proto))); } } // 2. Componentes propios de la página: se mueven, no se clonan. @@ -90,11 +76,11 @@ impl ChildrenInRegions { } } // 3. Prototipos del tema activo. - if let Some(theme_map) = themed.get(&theme.type_id()) { - if let Some(protos) = theme_map.get(region_name) { - for proto in protos { - result.add(proto.as_child()); - } + if let Some(theme_region) = THEME_REGIONS.read().get(&theme.type_id()) + && let Some(theme_protos) = theme_region.get(region_name) + { + for proto in theme_protos { + result.add(Child::from_arc(Arc::clone(proto))); } } @@ -168,8 +154,8 @@ impl InRegion { /// html! { "Aviso legal" } /// })); /// ``` - pub fn add(&self, component: impl Component + Clone + 'static) -> &Self { - let proto: Arc = Arc::new(component); + pub fn add(&self, component: impl Component) -> &Self { + let proto: Arc = Arc::new(component); match self { InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto), InRegion::Global(region) => Self::add_to_common(*region, proto), @@ -187,7 +173,7 @@ impl InRegion { } #[inline] - fn add_to_common(region: RegionRef, proto: Arc) { + fn add_to_common(region: RegionRef, proto: Arc) { COMMON_REGIONS .write() .entry(region.name().to_owned()) From 7e6a9f85578e2940877a61e1af01f94cb2606221 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 26 Jul 2026 12:50:33 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(core):=20Clave=20`&'s?= =?UTF-8?q?tatic=20str`=20en=20RegionComponents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/theme/regions.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 515337f3..f6ac2e18 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -9,11 +9,14 @@ use std::sync::{Arc, LazyLock}; // Mapea cada nombre de región con su lista de prototipos de componentes. // +// La clave es `&'static str` (lo que ya devuelve `RegionName::name()`) en lugar de `String`. No +// hace falta reservar en el heap una copia de un dato que ya vive de forma estática. +// // Se comparten como `Arc`. El prototipo no se clona al registrarse ni al ensamblar // la región (sólo se clona el `Arc`, barato). En cambio, sí se realiza un clonado del componente en // `Child::render()`, cuando cada petición necesita su propia copia mutable para pasar por `setup()` // desde un estado inicial limpio. -type RegionComponents = HashMap>>; +type RegionComponents = HashMap<&'static str, Vec>>; // Regiones globales con prototipos asociados a un tema específico. static THEME_REGIONS: LazyLock>> = @@ -27,7 +30,7 @@ static COMMON_REGIONS: LazyLock> = // Contenedor interno de componentes agrupados por región. #[derive(AutoDefault)] -pub(crate) struct ChildrenInRegions(HashMap); +pub(crate) struct ChildrenInRegions(HashMap<&'static str, Children>); impl ChildrenInRegions { pub fn with(region: RegionRef, child: Child) -> Self { @@ -42,7 +45,7 @@ impl ChildrenInRegions { region.alter_child(child); } else { let children = Children::new().with_child(child); - self.0.insert(region_name.to_owned(), children); + self.0.insert(region_name, children); } self } @@ -164,7 +167,7 @@ impl InRegion { .write() .entry(theme.type_id()) .or_default() - .entry((*region).name().to_owned()) + .entry((*region).name()) .or_default() .push(proto); } @@ -176,7 +179,7 @@ impl InRegion { fn add_to_common(region: RegionRef, proto: Arc) { COMMON_REGIONS .write() - .entry(region.name().to_owned()) + .entry(region.name()) .or_default() .push(proto); } From 74dbcde5c5736dc06a51f5cdf3db5f10ffdf3a89 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 26 Jul 2026 13:41:32 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(core):=20Cambia=20Has?= =?UTF-8?q?hMap=20x=20Vec=20en=20RegionComponents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/component/children.rs | 6 ++- src/core/theme/regions.rs | 69 ++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/core/component/children.rs b/src/core/component/children.rs index 519c2f2f..31d299a8 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -381,8 +381,12 @@ impl Children { // **< Children HELPERS >*********************************************************************** // Añade más de un componente hijo al final de la lista (en el orden recibido). + // + // Usa `Vec::extend()`, que reserva la capacidad necesaria de una vez a partir del `size_hint()` + // del iterador, en vez de una reasignación incremental por cada `push()`. También lo usa + // `core::theme::regions` para fusionar las fuentes de una región. #[inline] - fn add_many(&mut self, iter: I) -> &mut Self + pub(crate) fn add_many(&mut self, iter: I) -> &mut Self where I: IntoIterator, { diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index f6ac2e18..9543b31b 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -7,16 +7,39 @@ use parking_lot::RwLock; use std::collections::HashMap; use std::sync::{Arc, LazyLock}; -// Mapea cada nombre de región con su lista de prototipos de componentes. +// Lista de prototipos de componentes por nombre de región. +// +// Utiliza Vec en lugar de HashMap. El número de regiones registradas por tema o aplicación es casi +// siempre de un dígito, así que una búsqueda lineal por igualdad de `&str` evita el coste de +// hashear la clave. Además, el trabajo para recorrer regiones vacías es mínimo. // // La clave es `&'static str` (lo que ya devuelve `RegionName::name()`) en lugar de `String`. No // hace falta reservar en el heap una copia de un dato que ya vive de forma estática. -// -// Se comparten como `Arc`. El prototipo no se clona al registrarse ni al ensamblar -// la región (sólo se clona el `Arc`, barato). En cambio, sí se realiza un clonado del componente en -// `Child::render()`, cuando cada petición necesita su propia copia mutable para pasar por `setup()` -// desde un estado inicial limpio. -type RegionComponents = HashMap<&'static str, Vec>>; +#[derive(AutoDefault)] +struct RegionComponents(Vec<(&'static str, Vec>)>); + +impl RegionComponents { + // Devuelve los prototipos registrados para la región indicada, si hay alguno. + fn get(&self, region_name: &str) -> Option<&Vec>> { + self.0 + .iter() + .find(|(name, _)| *name == region_name) + .map(|(_, protos)| protos) + } + + // Añade un prototipo a la región indicada, creando la entrada si es la primera. + // + // Se comparte como `Arc`. El prototipo no se clona aquí ni al ensamblar la + // región (sólo se clona el `Arc`, barato). El único clonado real del componente ocurre en + // `Child::render()`, cuando cada petición necesita su propia copia mutable para pasar por + // `setup()` desde un estado inicial limpio. + fn push(&mut self, region_name: &'static str, proto: Arc) { + match self.0.iter_mut().find(|(name, _)| *name == region_name) { + Some((_, protos)) => protos.push(proto), + None => self.0.push((region_name, vec![proto])), + } + } +} // Regiones globales con prototipos asociados a un tema específico. static THEME_REGIONS: LazyLock>> = @@ -24,7 +47,7 @@ static THEME_REGIONS: LazyLock>> = // Regiones globales con prototipos comunes a todos los temas. static COMMON_REGIONS: LazyLock> = - LazyLock::new(|| RwLock::new(HashMap::new())); + LazyLock::new(|| RwLock::new(RegionComponents::default())); // ************************************************************************************************* @@ -68,23 +91,25 @@ impl ChildrenInRegions { // 1. Prototipos globales comunes. if let Some(global_protos) = COMMON_REGIONS.read().get(region_name) { - for proto in global_protos { - result.add(Child::from_arc(Arc::clone(proto))); - } + result.add_many( + global_protos + .iter() + .map(|proto| Child::from_arc(Arc::clone(proto))), + ); } // 2. Componentes propios de la página: se mueven, no se clonan. if let Some(page_children) = self.0.remove(region_name) { - for child in page_children { - result.add(child); - } + result.add_many(page_children); } // 3. Prototipos del tema activo. if let Some(theme_region) = THEME_REGIONS.read().get(&theme.type_id()) && let Some(theme_protos) = theme_region.get(region_name) { - for proto in theme_protos { - result.add(Child::from_arc(Arc::clone(proto))); - } + result.add_many( + theme_protos + .iter() + .map(|proto| Child::from_arc(Arc::clone(proto))), + ); } result @@ -167,9 +192,7 @@ impl InRegion { .write() .entry(theme.type_id()) .or_default() - .entry((*region).name()) - .or_default() - .push(proto); + .push((*region).name(), proto); } } self @@ -177,10 +200,6 @@ impl InRegion { #[inline] fn add_to_common(region: RegionRef, proto: Arc) { - COMMON_REGIONS - .write() - .entry(region.name()) - .or_default() - .push(proto); + COMMON_REGIONS.write().push(region.name(), proto); } }