//! API para añadir y gestionar nuevos temas. //! //! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos //! interactivos. Usa plantillas ([`Template`](crate::base::component::layout::Template)) para //! maquetar los contenidos en base a regiones ([`Region`](crate::base::component::layout::Region)). //! Cada región es un contenedor lógico identificado por un nombre para agrupar y renderizar //! componentes. //! //! Una página ([`Page`](crate::response::Page)) es un documento HTML completo. Implementa //! [`Contextual`](crate::core::component::Contextual) para gestionar su propio //! [`Context`](crate::core::component::Context), donde mantiene el tema activo, la plantilla //! seleccionada y los componentes asociados a cada región a renderizar. //! //! # Temas hijo, herencia y componentes //! //! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un tema //! hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos selectivamente. //! Esta herencia sólo determina qué implementación de sus métodos se usa cuando el tema hijo no los //! sobrescribe (como el renderizado del `` y del ``, los recursos incorporados, el uso //! de [`Theme::handle_component()`], las páginas de error, etc.). Un tema hijo puede ser a su vez //! padre de otro, basta declararlo cada vez con [`Theme::parent()`]. //! //! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación //! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese //! ciclo provocaría un bucle infinito en [`Theme::handle_component()`] o un desbordamiento de pila //! en los métodos predefinidos de `Theme` que delegan recursivamente en el padre. Para evitarlo, //! PageTop recorre la cadena de cada tema al registrarlo y **aborta el arranque de la aplicación** //! si detecta una referencia circular. //! //! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio //! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa //! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico //! (por ejemplo, un editor de texto enriquecido) pensado para usarse en cualquier aplicación, con //! independencia del tema activo. Que un tema decida capturar ese componente en //! [`Theme::handle_component()`] para adaptarlo es una decisión propia del tema, no una relación de //! parentesco: cualquier tema de la cadena de herencia puede interceptar cualquier componente, //! venga de la extensión que venga, sin que exista ningún vínculo de diseño previo entre ambos. //! //! Lo que sí es responsabilidad del tema activo es garantizar que el componente disponga de los //! recursos que necesita para verse y comportarse correctamente: sus propios estilos y JavaScript, //! si los aporta, o los que ofrezca el tema. El componente genera su marcado igual aunque esos //! recursos falten, pero el resultado seguramente no lucirá ni funcionará como se espera. //! //! # Cómo crear un tema nuevo //! //! Un tema mínimo es una extensión que implementa [`Extension`](crate::core::extension::Extension) //! y también [`Theme`] para que [`Extension::theme()`](crate::core::extension::Extension::theme) //! devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, ya que todos los //! métodos de [`Theme`] tienen implementación por defecto. //! //! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por //! defecto no basta: //! //! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegions`] (`Header`, `Aside`, //! `Content`, `Footer`) como regiones de plantilla siempre disponibles, y [`ReservedRegions`] //! (`PageTop`, `PageBottom`) como regiones reservadas que se renderizan al margen de cualquier //! plantilla. Un tema puede definir su propio *enum* que implemente [`RegionName`] para //! **añadir** nuevas regiones que PageTop no ofrece (como barras laterales, regiones específicas //! para menús, sliders, cabeceras hero, etc.). No es necesario redefinir las de [`CoreRegions`] //! ni las de [`ReservedRegions`], que ya existen y se asume que cualquier tema respeta. //! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplates`], con las plantillas //! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son //! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada //! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para //! **añadir** plantillas que PageTop no ofrece, y no para redefinir `Standard`/`Admin`. //! 3. **Cambiar cómo se renderiza** una región, una plantilla o un componente ya existente, se hace //! capturando el componente ([`Region`](crate::base::component::layout::Region) o //! [`Template`](crate::base::component::layout::Template), o el componente que sea) en //! [`Theme::handle_component()`]. En el caso de regiones y plantillas, para distinguir *qué* //! región o plantilla concreta envuelve el componente, sin comparar cadenas, basta con encadenar //! el *getter* correspondiente //! ([`Region::region()`](crate::base::component::layout::Region::region) o //! [`Template::template()`](crate::base::component::layout::Template::template)) con //! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por //! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente //! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias //! variantes de plantilla. //! //! Para forzar una plantilla completamente distinta en una página concreta, se puede llamar //! manualmente a [`with_template()`](crate::core::component::Contextual::with_template). //! //! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se //! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de //! navegación del sitio. Los temas pueden personalizar su contenido sobrescribiendo //! [`Theme::error_403()`], [`Theme::error_404()`] o [`Theme::error_fatal()`], sin necesidad de una //! plantilla distinta. //! //! El resto del comportamiento de un tema (por ejemplo, el renderizado del ``) se sobrescribe //! de forma independiente de estos tres pasos y no es necesario para tener un tema funcional. //! //! # Componentes que se procesan en todas las páginas //! //! Los componentes añadidos a una página con //! [`with_child_in()`](crate::core::component::Contextual::with_child_in) sólo existen para esa //! petición concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`] //! resuelve el caso contrario: un componente que se debe procesar en todas las páginas, o en todas //! las de un tema concreto, sin tener que registrarlo en el código de cada página. //! //! `InRegion` registra el componente una sola vez, normalmente al arrancar la aplicación o al //! inicializar una extensión, y a partir de ahí se procesa automáticamente en todas las páginas que //! correspondan: //! //! ```rust,no_run //! # use pagetop::prelude::*; //! InRegion::Global(&CoreRegions::Footer).add(PoweredBy::new()); //! ``` //! //! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del //! renderizado, de modo que su `setup()` siempre parte de un estado inicial limpio y no acumula //! mutaciones entre peticiones. //! //! Como cualquier otro componente, antes de renderizarse pasa por //! [`is_renderable()`](crate::core::component::Component::is_renderable), el primer paso del //! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una //! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o //! si el usuario actual está autenticado. //! //! [`ReservedRegions`]: crate::response::ReservedRegions use crate::AutoDefault; use crate::core::AnyInfo; use crate::locale::Lc; // **< RegionName >********************************************************************************* /// Interfaz común para las regiones lógicas del ``. /// /// Una [`RegionName`] representa un contenedor lógico identificado por un nombre de región. Su /// contenido se obtiene del [`Context`], donde los componentes suelen registrarse usando /// implementaciones de métodos como [`Contextual::with_child_in()`]. /// /// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas /// implementaciones de [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de /// componentes registrados en el [`Context`]. Un *enum* propio que implemente [`RegionName`] está /// pensado para **añadir** regiones que PageTop no ofrece (con un nombre propio que no colisione /// con los de [`CoreRegions`] o [`ReservedRegions`]). /// /// El tema decide qué regiones mostrar en el ``, normalmente usando una plantilla /// ([`TemplateName`]) al renderizar la página ([`Page`]). /// /// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante /// [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que un tema distinga en /// [`Theme::handle_component()`] qué variante concreta está renderizando el componente [`Region`]). /// /// [`Context`]: crate::core::component::Context /// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in /// [`ReservedRegions`]: crate::response::ReservedRegions /// [`Page`]: crate::response::Page /// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref /// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component /// [`Region`]: crate::base::component::layout::Region pub trait RegionName: Send + Sync + AnyInfo { /// Devuelve el nombre de la región. /// /// Este nombre es el identificador lógico de la región y se usa como clave en el /// [`Context`](crate::core::component::Context) para recuperar y renderizar el contenido /// registrado bajo ese nombre. Cualquier implementación de [`RegionName`] que devuelva el mismo /// nombre compartirá el mismo conjunto de componentes. fn name(&self) -> &'static str; /// Devuelve un *texto localizado* como etiqueta de accesibilidad asociada a la región. /// /// En la implementación predeterminada de [`Region`](crate::base::component::layout::Region), /// este valor se usa como `aria-label` del contenedor de la región. fn label(&self) -> Lc; } /// Referencia estática a una región. pub type RegionRef = &'static dyn RegionName; // **< CoreRegions >******************************************************************************** /// Regiones básicas que PageTop proporciona por defecto. /// /// Comparten sus nombres (`"header"`, `"aside"`, `"content"`, `"footer"`) con otras regiones que /// implementen [`RegionName`], por lo que comparten también el contenido registrado bajo esos /// nombres. Por defecto, son las regiones usadas por [`Template`]. /// /// A estas regiones hay que sumar también las regiones internas reservadas por [`ReservedRegions`] /// (`"page-top"` y `"page-bottom"`), que [`Page::render()`] renderiza en cualquier caso. /// /// [`Template`]: crate::base::component::layout::Template /// [`ReservedRegions`]: crate::response::ReservedRegions /// [`Page::render()`]: crate::response::Page::render #[derive(AutoDefault)] pub enum CoreRegions { /// Región estándar para la **cabecera** del documento, de nombre `"header"`. /// /// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc. Header, /// Región de **contenido secundario**, de nombre `"aside"`. /// /// Se renderiza por defecto entre `Header` y `Content`. Un tema podría maquetarla, por ejemplo, /// como columna lateral junto a `Content` y emplearla para menús secundarios o cualquier otro /// contenido complementario al principal. Aside, /// Región principal de **contenido**, de nombre `"content"`. /// /// Es la región donde se renderiza el contenido principal del documento. En general será la /// región mínima imprescindible para que una página tenga sentido. #[default] Content, /// Región estándar para el **pie de página**, de nombre `"footer"`. /// /// Suele contener información legal, enlaces secundarios, créditos, etc. Footer, } impl RegionName for CoreRegions { #[inline] fn name(&self) -> &'static str { match self { Self::Header => "header", Self::Aside => "aside", Self::Content => "content", Self::Footer => "footer", } } #[inline] fn label(&self) -> Lc { match self { Self::Header => Lc::l("region_header"), Self::Aside => Lc::l("region_aside"), Self::Content => Lc::l("region_content"), Self::Footer => Lc::l("region_footer"), } } } // **< TemplateName >******************************************************************************* /// Interfaz común para las plantillas lógicas de una página. /// /// Representa una variante identificada por un nombre. Un tema puede usar este nombre para decidir /// la composición del cuerpo de una página ([`Page`](crate::response::Page)), es decir, qué /// regiones ([`RegionName`]) renderizar y en qué orden. /// /// Requiere [`AnyInfo`] por el mismo motivo que [`RegionName`], para que un [`TemplateRef`] pueda /// recuperarse mediante [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su /// tipo concreto (por ejemplo, para que un tema distinga en /// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante concreta /// está renderizando el componente [`Template`](crate::base::component::layout::Template)). pub trait TemplateName: Send + Sync + AnyInfo { /// Devuelve el nombre de la plantilla. fn name(&self) -> &'static str; /// Devuelve un *texto localizado* como etiqueta descriptiva de la plantilla. fn label(&self) -> Lc; } /// Referencia estática a una plantilla. pub type TemplateRef = &'static dyn TemplateName; // **< CoreTemplates >****************************************************************************** /// Plantillas que PageTop proporciona por defecto. #[derive(AutoDefault)] pub enum CoreTemplates { /// Plantilla predeterminada, de nombre `"standard"`. /// /// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente. #[default] Standard, /// Plantilla para la **interfaz de administración**, de nombre `"admin"`. /// /// Se utiliza para páginas de administración o paneles de control. Admin, } impl TemplateName for CoreTemplates { #[inline] fn name(&self) -> &'static str { match self { Self::Standard => "standard", Self::Admin => "admin", } } #[inline] fn label(&self) -> Lc { match self { Self::Standard => Lc::l("template-standard"), Self::Admin => Lc::l("template-admin"), } } } // **< render_component! >************************************************************************** /// Sobrescribe el renderizado de componentes en /// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component). /// /// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra /// coincidencia, devuelve `Some(Ok(markup))` o `Some(Err(e))` según el resultado de la expresión /// asociada. Si ningún tipo coincide, devuelve `None` para que el sistema continúe con la cadena de /// herencia o con el renderizado por defecto del propio componente. /// /// # Ejemplo /// /// ```rust,ignore /// fn handle_component( /// &self, /// component: &dyn Component, /// cx: &mut Context, /// ) -> Option> { /// render_component!(component, { /// Button => |btn| { Ok(html! { button.btn.btn-primary { (btn.label()) } }) }, /// Heading => |h| self.render_heading(h, cx), /// }) /// } /// /// fn render_heading(&self, h: &Heading, cx: &mut Context) -> Result { /// Ok(html! { h2.display-4 { (h.text()) } }) /// } /// ``` #[macro_export] macro_rules! render_component { ($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => { 'render_component: { // Reborrow explícito como referencia compartida para que `downcast_ref` funcione // correctamente con `&mut dyn Component` (limitación del compilador con trait objects). let __c = &*($component); $( if let Some($var) = __c.downcast_ref::<$type>() { break 'render_component Some($body); } )* None } }; } // **< setup_component! >*************************************************************************** /// Muta un componente dentro de /// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component). /// /// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra /// coincidencia, ejecuta el bloque asociado y detiene la evaluación. Si ningún tipo coincide, no /// hace nada. /// /// Usa acceso mutable al componente mediante [`downcast_mut`](crate::core::AnyCast::downcast_mut), /// lo que permite modificar su estado. El tema puede devolver `None` tras la mutación para que otro /// nivel de la cadena se encargue del renderizado. /// /// # Ejemplos /// /// Solo mutación: el tema ajusta el componente y delega el renderizado al siguiente nivel: /// /// ```rust,ignore /// fn handle_component( /// &self, /// component: &mut dyn Component, /// cx: &mut Context, /// ) -> Option> { /// setup_component!(component, { Button => |btn| { btn.add_class("btn-primary"); } }); /// None /// } /// ``` /// /// Mutación y renderizado combinados: el `Button` se muta y se renderiza aquí; el `Heading` se /// muta pero continúa la cadena para que otro nivel lo renderice: /// /// ```rust,ignore /// fn handle_component( /// &self, /// component: &mut dyn Component, /// cx: &mut Context, /// ) -> Option> { /// setup_component!(component, { /// Button => |btn| { btn.add_class("btn-primary"); }, /// Heading => |h| { h.add_class("display-4"); }, /// }); /// render_component!(component, { /// Button => |btn| Ok(html! { button.btn { (btn.label()) } }), /// }) /// } /// ``` #[macro_export] macro_rules! setup_component { ($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => { 'setup_component: { $( if let Some($var) = ($component).downcast_mut::<$type>() { $body; break 'setup_component; } )* } }; } // **< Definitions >******************************************************************************** mod definition; pub use definition::{Theme, ThemeRef}; mod regions; pub(crate) use regions::ChildrenInRegions; pub use regions::InRegion; pub(crate) mod all;