//! llimphi-compositor — el núcleo declarativo de Llimphi, sin winit. //! //! Aquí vive el árbol de vista `View` (DSL declarativo), su instalación //! sobre taffy (`mount`), el pintado a `vello::Scene` (`paint`/`paint_gpu`) y //! el hit-test. Nada de esto necesita una ventana ni `llimphi-hal`: la //! composición `view → layout → scene` es pura y reutilizable. //! //! El runtime que la maneja vive aparte: //! - `llimphi-ui` la corre sobre winit (`run()`). //! - a futuro, un runtime sobre el framebuffer del kernel `wawa` puede //! reusar exactamente este compositor sin arrastrar winit. //! //! `wgpu` entra sólo por la firma de [`GpuPaintFn`] (tipos de Device/Queue/ //! Encoder/TextureView); `wgpu` no depende de winit, así que el compositor //! sigue libre de windowing. use std::collections::HashMap; use std::sync::Arc; use llimphi_layout::taffy::NodeId; use llimphi_layout::{ComputedLayout, LayoutTree, Style}; use vello::kurbo::{ Affine, Ellipse, Point, Rect as KurboRect, RoundedRect, RoundedRectRadii, Stroke, }; use vello::peniko::{BlendMode, Color, Fill, Gradient, ImageBrush as Image, Mix}; mod anim; mod hero; mod layout_builder; mod render; mod ripple; mod semantics; mod view; pub use anim::{ ease_out_cubic, reconcile_size_anim, Anim, AnimRegistry, SizeAnim, SizeAnimRegistry, }; pub use hero::{Hero, HeroRegistry}; pub use layout_builder::{collect_builder_constraints, expand_layout_builders, has_layout_builder}; pub use render::*; pub use ripple::{Ripple, RippleRegistry}; pub use semantics::{Role, SemanticsFlags, SemanticsSpec}; /// Texto a pintar dentro de un nodo. Alineación por defecto `Center` /// (horizontal y vertical), apta para labels de botón. Para layouts tipo /// editor o párrafo, usar `.text_aligned(...)` con `Alignment::Start`. #[derive(Clone)] pub struct TextSpec { pub content: String, pub size_px: f32, pub color: Color, pub alignment: llimphi_text::Alignment, /// `true` = forzar variante italic en la fuente activa. Default false. pub italic: bool, /// Peso de fuente CSS: 400 = normal, 700 = bold. parley elige la /// variante más cercana de la familia activa (o la sintetiza). Se usa /// tanto al **medir** como al **pintar**, así medida y dibujo coinciden. /// Default 400. pub weight: f32, /// Límite de líneas (CSS `-webkit-line-clamp` / Flutter `maxLines`). `None` /// = sin límite (envuelve libre). Cuando el texto excede, se trunca: con /// [`Self::ellipsis`] la última línea termina en `…`, sin él se corta seco. /// Afecta medida (taffy reserva el alto de N líneas) y pintado. pub max_lines: Option, /// Si `true` y `max_lines` trunca, la última línea visible termina en `…`. /// Sin efecto si `max_lines` es `None`. Default false. pub ellipsis: bool, /// CSS-style font-family string (acepta lista con fallbacks). `None` /// = la fuente default de parley. pub font_family: Option, /// Múltiplo de interlínea (`line-height` / `font-size`). 1.2 es el /// default que usaban todos los callers; puriy lo sobreescribe con el /// valor computado de CSS. Se usa tanto al **medir** (para que taffy /// reserve el alto correcto) como al **pintar**, así medida y dibujo /// coinciden. pub line_height: f32, /// Colores por rango de **bytes** sobre `content`, para texto multicolor /// (syntax highlighting) en una sola pasada de shaping. `None` = color /// uniforme (`color`). Cuando es `Some`, el runtime usa /// `Typesetter::layout_runs` + `draw_layout_runs`, y `color` actúa como /// color por defecto de lo no cubierto por ningún run. pub runs: Option>, /// Subrayado activo. El runtime pinta la línea bajo la línea base usando /// las métricas (`underline_offset`, `underline_size`) que parley deriva /// de la fuente — así un texto a 12pt y otro a 24pt tienen un subrayado /// proporcional sin que el caller calcule nada. pub underline: bool, /// Tachado activo. Mismo régimen que [`Self::underline`] pero sobre el /// strikethrough metric — útil para listas to-do, items removidos en un /// diff, precios viejos. pub strikethrough: bool, /// **Spans inline mixtos** (RichText): overrides de /// tamaño/peso/italic/familia/color/underline/strikethrough por rango /// de bytes (parley convention). `None` = texto uniforme (camino /// `layout_clamped`); `Some([])` se trata como `None`. Cuando hay /// spans, el runtime usa `Typesetter::layout_spans` (Layout /// con `max_width`/wrap) + `draw_layout_runs_xf`; los campos del /// `TextSpec` son **defaults a nivel bloque** que cada span puede /// sobreescribir. Tier 2 final de PARIDAD-FLUTTER (Bloque 13). pub spans: Option>, /// `letter-spacing`: px **extra** entre letras (CSS). 0 = normal. Afecta /// shaping y medida. Sólo el camino uniforme (`layout_clamped`); el camino /// de spans (RichText) lo ignora en v1. pub letter_spacing: f32, /// `word-spacing`: px **extra** entre palabras (CSS). 0 = normal. Mismo /// régimen que [`Self::letter_spacing`]. pub word_spacing: f32, /// `white-space: nowrap`/`pre`: si `true`, el texto **no envuelve** — /// se shapea en una sola línea (`break_all_lines(None)`) sin importar el /// ancho disponible, y desborda la caja (lo recorta `overflow: hidden` si /// lo hay). Afecta medida (taffy reserva el ancho de la línea completa) y /// pintado. Default false (wrap libre, comportamiento previo). Sólo el /// camino uniforme (`layout_clamped`); el de spans (RichText) lo ignora en /// v1, igual que el clamp. pub no_wrap: bool, /// `overflow-wrap: break-word`/`anywhere` (o `word-break: break-all`): si /// `true`, una palabra más ancha que la caja se **parte** para que entre, /// en vez de desbordar. Afecta medida (taffy puede reservar menos ancho) y /// pintado. Default false (la palabra larga desborda — comportamiento /// previo). Sólo el camino uniforme (`layout_clamped`); el de spans /// (RichText) lo ignora en v1, igual que `no_wrap`/clamp. pub overflow_wrap: bool, } /// Fase de un drag activo. `Move` se emite por cada `CursorMoved` con el /// delta desde el evento anterior; `End` se emite al soltar el botón. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DragPhase { Move, End, } /// Handler de drag. Recibe la fase + delta (`dx`, `dy`) **desde el evento /// anterior** (no acumulado desde el press). Devolver `None` deja el drag /// activo sin disparar Msg. `Arc` para que el runtime pueda /// clonarlo barato al iniciar el drag y mantenerlo vivo aunque el cache /// de la vista se regenere mientras tanto. pub type DragFn = Arc Option + Send + Sync>; /// Handler de drop. El runtime lo invoca cuando un drag activo se suelta /// sobre este nodo. Recibe el `payload` `u64` que el origen del drag /// declaró vía [`View::drag_payload`]. Devolver `None` ignora el drop. /// /// Los IDs `u64` son opacos para el runtime: el widget elige una /// convención (índice de tile, hash del item, etc.) y el handler decide /// qué Msg emitir en función de ese ID. pub type DropFn = Arc Option + Send + Sync>; /// Handler de click con posición. Recibe `(x_local, y_local, rect_w, /// rect_h)`: las dos primeras son la posición del cursor **relativa a /// la esquina superior-izquierda del nodo** y las dos últimas son el /// ancho/alto actual del nodo en pixels — útil cuando el caller /// necesita centrar o normalizar. Devolver `None` no dispara update. pub type ClickAtFn = Arc Option + Send + Sync>; /// Handler de rueda **local a un nodo**. Recibe el delta `(dx, dy)` en /// líneas lógicas (misma normalización que `App::on_wheel`: `dy` positivo /// = scroll hacia abajo). El runtime lo invoca cuando la rueda gira con el /// cursor sobre este nodo, ANTES de caer al `App::on_wheel` global: si el /// handler devuelve `Some(Msg)`, el evento se consume acá. Permite áreas /// de scroll autocontenidas (el widget `scroll` lo usa) sin que cada app /// rutee la rueda a mano por su `Model`. Devolver `None` deja pasar el /// evento al `on_wheel` global. pub type ScrollFn = Arc Option + Send + Sync>; /// Variante de [`DragFn`] que **conoce la posición inicial del press** /// relativa al rect del nodo. Útil cuando el caller necesita identificar /// qué entidad (Concepto, lemming, etc.) bajo el cursor agarró el drag. /// Recibe `(phase, dx, dy, initial_lx, initial_ly)`. pub type DragAtFn = Arc Option + Send + Sync>; /// Variante de [`DragFn`] que recibe la **velocidad del drag al soltarlo** /// (`vx`, `vy` en px/s). El runtime mide el desplazamiento sobre los /// últimos ~100 ms de movimiento (ventana móvil de hasta ocho samples) /// y la pasa en `DragPhase::End`. Durante `DragPhase::Move` ambas son /// `0.0` — la velocidad sólo es significativa al final. Permite /// **fling-desde-drag**: el caller arranca un ticker con esa velocidad y /// la decae con [`fling_step`](https://docs.rs/) hasta asentar. Reemplaza /// la estimación manual que antes tenía que llevar el caller con /// `Instant::now()` por su cuenta. pub type DragVelocityFn = Arc Option + Send + Sync>; /// Fase de un **gesto continuo** (pinch-to-zoom de momento; rotación a futuro). /// El runtime emite `Begin` al iniciar el gesto, `Update` por cada cambio /// incremental y `End` al terminar. El camino de Ctrl+rueda (universal, sin /// trackpad) emite un único `Update` por click de rueda — no hay un "inicio" /// ni "fin" naturales, así que el handler debe tolerar `Update`s sueltos sin /// `Begin` previo (es lo común en desktop). El camino de trackpad /// (`PinchGesture`, sólo macOS/iOS) sí entrega `Begin`/`Update*`/`End`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GesturePhase { Begin, Update, End, } /// Handler de gesto de **escala** (pinch-to-zoom). Recibe `(phase, factor, /// focal_x, focal_y)`: /// - `factor`: cambio de escala **incremental y multiplicativo** desde el /// evento anterior — `1.0` = sin cambio, `>1.0` agranda (zoom in), `<1.0` /// achica (zoom out). El caller acumula con `mi_zoom *= factor` y, si /// quiere, lo clampa a su rango. En `Begin`/`End` el factor es `1.0`. /// - `focal_x`/`focal_y`: punto focal del gesto **relativo a la esquina /// superior-izquierda del rect del nodo** (mismo espacio que los handlers /// `*_at`). Es el punto que debe quedar fijo bajo el cursor al hacer zoom — /// el caller lo usa para zoomear "hacia el cursor" en vez de hacia el /// centro. En Ctrl+rueda es la posición del cursor; en trackpad, idem. /// /// Devolver `Some(Msg)` dispara una transición; `None` ignora el evento. El /// runtime lo resuelve con [`hit_test_scale`]: el nodo más al frente bajo el /// cursor que declare un `on_scale` consume el gesto. Es la base del zoom de /// los canvases (pineal/cosmos/nakui). pub type ScaleFn = Arc Option + Send + Sync>; /// Handler de gesto de **rotación** (trackpad, sólo macOS — winit no emite /// `RotationGesture` en Wayland/Windows). Análogo a [`ScaleFn`] pero el /// segundo argumento es el **delta de ángulo incremental en radianes** /// (positivo = horario) en lugar del factor de escala; `(focal_x, focal_y)` /// es el punto bajo el cursor relativo al rect del nodo. El nodo más al /// frente bajo el cursor que declare un `on_rotate` consume el gesto. Base /// para rotar canvases/imágenes con dos dedos. Ver [`View::on_rotate`]. pub type RotateFn = Arc Option + Send + Sync>; /// Restricciones de tamaño que un [`LayoutBuilderFn`] recibe: las dimensiones /// del slot que el layout le asignó al nodo (en px físicos). Análogo a las /// `BoxConstraints` de Flutter `LayoutBuilder` / al `MediaQuery` pero **local /// al nodo** (no a la ventana). El builder construye su subárbol en función de /// esto — p. ej. una columna si `max_width < 600`, dos si es ancho. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Constraints { pub max_width: f32, pub max_height: f32, } /// Constructor **diferido** de subárbol sensible al tamaño (Flutter /// `LayoutBuilder`). El runtime resuelve el tamaño del slot del nodo en una /// primera pasada de layout y luego invoca esta closure con esas /// [`Constraints`] para producir los hijos — así "construir distinto según el /// espacio disponible" deja de exigir conocer el tamaño al armar el `View`. Ver /// [`View::layout_builder`]. pub type LayoutBuilderFn = Arc View + Send + Sync>; /// Rect absoluto del nodo (en coordenadas físicas del frame). Lo /// recibe el callback de [`View::paint_with`] para que pueda /// posicionar sus primitivas custom dentro del nodo. #[derive(Debug, Clone, Copy, Default)] pub struct PaintRect { pub x: f32, pub y: f32, pub w: f32, pub h: f32, } /// Callback de pintura custom. El runtime lo invoca durante el paint /// del nodo (entre el `fill`/`image` y el `text`) con el `Scene` vivo /// + el `Typesetter` cacheado del runtime + el rect absoluto del nodo. /// Pensado para "canvas elements" tipo `dominium-canvas`, /// `pluma-editor` (osciloscopio de coherencia), `cosmos` (charts). /// /// El `Typesetter` se pasa porque crearlo por frame es caro /// (`FontContext::new` enumera las fontes del sistema vía fontique). /// Los callers que no necesiten texto pueden ignorar el argumento. /// /// El callback no debe llamar a `scene.push_layer` sin un `pop_layer` /// correspondiente, ni reset el scene — sólo agregar primitivas que /// pertenezcan al rect del nodo. pub type PaintFn = Arc< dyn Fn(&mut vello::Scene, &mut llimphi_text::Typesetter, PaintRect) + Send + Sync, >; /// Callback de pintura GPU directo, sin vello intermedio. Recibe el /// `device`/`queue` ya construidos por el runtime más un /// `CommandEncoder` y la `TextureView` del frame (la intermediate /// `Rgba8Unorm` de `WinitSurface`), todo durante el paint del nodo. /// /// El caller abre su propio `begin_render_pass` con `LoadOp::Load` para /// no sobrescribir lo que ya pintó vello, dibuja sus primitivas y /// cierra el pass. El runtime se encarga de dispatchear (`queue.submit`) /// el encoder ya con todas las pasadas de todos los nodos acumuladas — /// es un solo submit por frame. /// /// **Orden de pintura en Fase 1**: todos los `gpu_painter` corren /// DESPUÉS de la pasada completa de vello (fill, image, painter, /// text) sobre el `mounted` tree. Entre sí mantienen el orden DFS /// pre-orden. Si una app necesita pintar texto **encima** del render /// GPU directo, la forma idiomática es ponerlo en `App::view_overlay`, /// que se renderiza como una segunda Scene de vello encima de todo. /// /// Pensado para apps con volumen masivo de primitivos (cosmos /// starfield Gaia, tinkuy particle viewer, nakui viewport, pineal /// denso) — el hook que paga el costo de mantener pipelines WGSL /// propias en `llimphi-raster` (ver `02_ruway/llimphi/SDD.md` /// §"Roadmap — GPU directo wgpu"). pub type GpuPaintFn = Arc< dyn Fn( &wgpu::Device, &wgpu::Queue, &mut wgpu::CommandEncoder, &wgpu::TextureView, PaintRect, (u32, u32), ) + Send + Sync, >; /// Callback de pintura vello "over": idéntico en firma a [`PaintFn`] /// `(&mut Scene, &mut Typesetter, PaintRect)`, pero el runtime lo invoca /// en una pasada vello FINAL, **después** de todos los `gpu_painter` del /// frame. Sus primitivas se rasterizan sobre fondo transparente y se /// componen con alpha encima de la intermedia (que ya tiene /// vello-base + GPU directo). Resuelve el z-order inverso al de /// [`GpuPaintFn`]: permite pintar texto/sprites AA por vello **encima** /// de celdas instanciadas por GPU (dominium grid, futuro motor voxel). /// /// Orden total del frame: `[vello base] → [gpu_painter] → [over_painter] /// → [overlay/menús]`. Los menús (`view_overlay`) siguen quedando por /// encima del over-layer. Ver [`View::paint_over`]. Es un alias de /// [`PaintFn`]; existe sólo para documentar la semántica temporal. pub type OverPaintFn = PaintFn; /// Sombra proyectada detrás del rect del nodo (drop shadow), rasterizada /// con el `draw_blurred_rounded_rect` nativo de vello. Se pinta **antes** /// del relleno, así el fill (si es opaco) tapa la parte solapada y la /// sombra sólo asoma por el desenfoque + el offset. El radio sigue al del /// nodo (más `spread`). #[derive(Clone, Copy, Debug, PartialEq)] pub struct Shadow { pub color: Color, /// Desviación estándar del gaussiano (qué tan difusa). En px. pub blur: f64, /// Desplazamiento de la sombra respecto del nodo. pub dx: f64, pub dy: f64, /// Cuánto crece (px) el rect de la sombra respecto del nodo. pub spread: f64, } impl Shadow { /// Sombra con color + blur explícitos, sin offset ni spread. pub fn new(color: Color, blur: f64) -> Self { Self { color, blur, dx: 0.0, dy: 0.0, spread: 0.0 } } /// Elevación suave y tasteful: negro translúcido, leve caída hacia /// abajo. El default razonable para cards/menús/modales. pub fn soft(alpha: u8, blur: f64) -> Self { Self { color: Color::from_rgba8(0, 0, 0, alpha), blur, dx: 0.0, dy: blur * 0.4, spread: 0.0, } } pub fn offset(mut self, dx: f64, dy: f64) -> Self { self.dx = dx; self.dy = dy; self } pub fn spread(mut self, spread: f64) -> Self { self.spread = spread; self } } /// Borde (stroke) pintado sobre el contorno redondeado del nodo, **inset** /// hacia adentro media línea para que el grosor quede dentro del rect /// (convención CSS `box-sizing: border-box`). Se pinta después del relleno. #[derive(Clone, Copy, Debug)] pub struct Border { pub width: f64, pub color: Color, } impl Border { pub fn new(width: f64, color: Color) -> Self { Self { width, color } } } /// Una operación de filtro CSS (`filter: blur()/brightness()/…`) aplicada al /// **propio subárbol** del nodo. A diferencia de `backdrop_blur` (que afecta lo /// pintado *debajo*), un `FilterOp` modifica el contenido del nodo. El runtime /// los aplica como post-pasada GPU sobre la intermediate, restringidos al rect /// del nodo, en el orden de la lista. La lista crece por fase (CSS Filter /// Effects 1): `Blur` (7.1232) + `ColorMatrix` (7.1233). Fase 7.1232. #[derive(Clone, Debug, PartialEq)] pub enum FilterOp { /// `filter: blur()`. `px` es la desviación estándar del Gauss (igual /// convención que CSS). Se aplica con `BlurCompositor`, el mismo camino que /// `backdrop_blur`. Blur(f32), /// Filtros de color (`brightness`/`contrast`/`grayscale`/`sepia`/`saturate`/ /// `invert`/`hue-rotate`/`opacity`) colapsados a una **matriz de color 4×5** /// row-major: por fila `[c0, c1, c2, c3, bias]`, salida R/G/B/A /// (`out = M·rgba + bias`). Se aplica con `ColorFilterCompositor`. Fase /// 7.1233. ColorMatrix([f32; 20]), /// `filter: drop-shadow( [blur] [color])`. Se pinta como una sombra /// Gaussiana del **border-box** detrás del nodo (con `draw_blurred_rounded_rect`, /// igual primitiva que `Shadow`/box-shadow). v1: sombra del rect, no de la /// silueta alpha del subárbol. A diferencia de `Blur`/`ColorMatrix`, NO es /// post-pasada GPU — se pinta en vello antes del relleno, por lo que /// `collect_filters` la ignora. Fase 7.1234. DropShadow(Shadow), } /// Punto de pivote de `transform` (CSS `transform-origin`). Cada eje se resuelve /// contra el rect del nodo como `px + frac · tamaño`: `px` (ya escalado por zoom /// por el caller) cubre offsets absolutos y `frac` los porcentuales (`0.5` = 50% /// del ancho/alto). El default CSS `50% 50%` (centro) es /// `{ px: (0.0, 0.0), frac: (0.5, 0.5) }`; un nodo con `transform_origin: None` /// usa ese centro. Modela `px + %` por eje igual que `transform_rel` modela el /// `translate(<%>)` — necesario porque el % depende del layout, desconocido hasta /// `paint`. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TransformPivot { /// Offset absoluto en px (ya × zoom) por eje `(x, y)`. pub px: (f64, f64), /// Fracción del tamaño del rect por eje `(x, y)` (`0.5` = 50%). pub frac: (f64, f64), } impl Default for TransformPivot { fn default() -> Self { // CSS `transform-origin: 50% 50%` — centro del rect. Self { px: (0.0, 0.0), frac: (0.5, 0.5) } } } /// Nodo de la vista declarativa. Estilo de layout (taffy) + relleno opcional /// (vello) + texto opcional (skrifa+vello) + Msg al click opcional + hijos. pub struct View { pub style: Style, pub fill: Option, /// Relleno cuando el cursor está sobre este nodo. Sin valor (`None`) /// = no se reacciona al hover. pub hover_fill: Option, pub radius: f64, /// Radio **por esquina** (top-left, top-right, bottom-right, bottom-left), /// que sobreescribe a `radius` cuando está presente. Permite cards con /// sólo las esquinas de arriba redondeadas, pestañas, bocadillos de chat, /// etc. (CSS `border-radius` con 4 valores). `None` = usar el `radius` /// uniforme. Ver [`View::radius_corners`]. La **sombra** sigue usando un /// radio escalar (el blur nativo de vello no acepta radios por esquina); /// el **borde** sí respeta las cuatro esquinas. pub corner_radii: Option, /// Sombra proyectada detrás del nodo (drop shadow). `None` = sin sombra /// (la mayoría de nodos). Ver [`Shadow`]. pub shadow: Option, /// Relleno con **gradiente**, autoreado en el cuadrado unidad `[0,1]²` y /// mapeado al rect del nodo. Gana sobre `fill` como base; `hover_fill` /// (un color) lo sigue overrideando en hover. Ver [`View::fill_gradient`]. pub fill_gradient: Option, /// Borde (stroke) sobre el contorno redondeado. Ver [`Border`]. pub border: Option, pub text: Option, /// Imagen a pintar dentro del rect del nodo. Se centra y escala /// según [`Self::image_fit`] (default `Contain` = preservar /// aspect ratio cabiendo). El alfa por píxel de la imagen y el /// `Image::alpha` global se respetan; el `fill` (si lo hay) se /// pinta debajo como background. El clip al `node_rrect` respeta /// `radius`/`corner_radii`, así avatares y cards con esquinas /// redondeadas funcionan sin envolver en un padre `clip(true)`. pub image: Option, /// Política de encaje de [`Self::image`] en el rect del nodo /// (CSS `object-fit`). `None` = `Contain` (el default histórico). /// Ver [`ImageFit`] y [`View::image_fit`]. pub image_fit: Option, /// **Máscara de luminancia** (CSS `mask-image`). Si está presente, el /// runtime aísla el subárbol del nodo en una capa y luego lo enmascara con /// la luminancia de esta imagen (`push_luminance_mask_layer` de vello): /// blanco = visible, negro = oculto, gris = semitransparente. El encaje lo /// fija [`Self::mask_placement`] (size/position/repeat); sin él la imagen se /// estira al border-box. `None` = sin máscara. Ver [`View::mask_image`]. pub mask_image: Option, /// Encaje de [`Self::mask_image`] (CSS `mask-size`/`-position`/`-repeat`). /// `None` = estirar al border-box (Fase 7.1226). Sólo se consulta si /// `mask_image` está presente. Ver [`MaskPlacement`] y /// [`View::mask_placement`]. Fase 7.1227. pub mask_placement: Option, /// Capas de máscara ADICIONALES (`mask-image: url(a), url(b), …`): cada una /// es `(imagen, operador)`. Comparten [`Self::mask_placement`] con la capa 0 /// ([`Self::mask_image`]); se combinan con ella según el operador. Vacío = /// una sola capa. Ver [`View::mask_extra`]. Fase 7.1231. pub mask_extra: Vec<(Image, MaskCompose)>, /// Callback de pintura custom. Si está presente, el runtime lo /// invoca durante el paint del nodo con el `Scene` vivo + el rect /// absoluto. Pensado para "canvas elements" (dominium, pluma, /// cosmos) que pintan primitivas custom no expresables como una /// composición de Views. pub painter: Option, /// Pintor GPU directo. Se invoca DESPUÉS de la pasada vello del /// frame; comparte tree y orden DFS con los demás. Ver /// [`GpuPaintFn`]. pub gpu_painter: Option, /// Pintor vello "over": closure que pinta DESPUÉS del pase GPU del /// frame, sobre una escena vello que el runtime compone con alpha /// encima de la intermedia. Sirve para sprites/texto AA encima de /// celdas instanciadas por GPU. Ver [`View::paint_over`] y /// [`OverPaintFn`]. Misma firma que [`PaintFn`] — sólo cambia /// *cuándo* corre (post-GPU). `None` = sin over-layer (coste cero). pub over_painter: Option, pub on_click: Option, /// Handler de click que recibe la posición **relativa al rect del /// nodo** (esquina superior-izquierda del nodo = `(0, 0)`). Útil /// para canvas elements que quieren mapear el click a coordenadas /// de mundo. Si está presente, gana sobre `on_click`. Devolver /// `None` no dispara update. pub on_click_at: Option>, /// Equivalente a `on_click` pero para el botón derecho del ratón. /// Pensado para menús contextuales: el nodo declara qué `Msg` /// emitir cuando se le hace right-click, y la app abre el overlay /// con el menú. pub on_right_click: Option, /// Variante posicional de [`Self::on_right_click`]. Útil para /// grillas que necesitan saber *qué celda* del rect recibió el /// click derecho (la celda no es un nodo aparte, sino una región /// dentro del nodo). Si está presente, gana sobre `on_right_click`. pub on_right_click_at: Option>, /// Equivalente a `on_click` pero para el botón del medio del ratón /// (rueda presionada). Pensado para abrir en pestaña nueva — los /// browsers usan middle-click como atajo equivalente a Ctrl+Click. pub on_middle_click: Option, /// Handler de drag. Si está presente, este nodo arrastra (y NO emite /// `on_click` al presionar — un nodo es uno u otro). pub drag: Option>, /// Variante de drag que recibe la posición inicial del press relativa /// al rect del nodo. Gana sobre `drag` si ambos están presentes. pub drag_at: Option>, /// Variante de drag que recibe la **velocidad** al soltar (`vx`, `vy` /// en px/s) además del delta puntual. Gana sobre `drag`/`drag_at` /// cuando está presente — un nodo elige un único sabor de drag. Habilita /// fling-desde-drag (el caller arranca un ticker con esa velocidad y la /// decae con [`fling_step`]). pub drag_velocity: Option>, /// Payload `u64` que viaja con el drag iniciado sobre este nodo. Lo /// recibe el handler [`Self::on_drop`] del drop target. Sin payload, /// el drag funciona igual pero ningún drop target reacciona. pub drag_payload: Option, /// Handler invocado al soltar un drag sobre este nodo (drop target). pub on_drop: Option>, /// Color a pintar mientras un drag activo está hovereando este drop /// target. Sobrepone a `fill`/`hover_fill` cuando aplica. pub drop_hover_fill: Option, /// Si `true`, los descendientes se recortan al rect del nodo (vía /// `scene.push_layer` con `Mix::Clip`). El hit-test también respeta /// el recorte: clicks fuera del rect ignoran a los hijos. pub clip: bool, /// Si `Some([top, right, bottom, left])`, recorta los descendientes a un /// rect ENCOGIDO por esos insets (px) desde el rect del nodo — modela /// `clip-path: inset(...)`. Implica clip aunque `clip == false`. pub clip_inset: Option<[f32; 4]>, /// Si `Some(spec)` (14 floats), recorta los descendientes a una ELIPSE — /// modela `clip-path: circle()`/`ellipse()`. El centro (4) se resuelve /// contra el rect: `cx = cx_px + cx_pct/100·w`, `cy = cy_px + /// cy_pct/100·h`. Cada radio (5: `[px, pct_w, pct_h, pct_diag, side]`) con /// `side == 0` suma `px + pct_w/100·w + pct_h/100·h + pct_diag/100·diag` /// (`diag = √(w²+h²)/√2`); con `side != 0` se computa desde la distancia /// del centro a los bordes (`1`/`2` = closest/farthest sobre los 4 lados; /// `3`/`4` = ídem sobre el eje del radio). Layout: `[cx×2, cy×2, rx×5, /// ry×5]`. Implica clip aunque `clip == false`. Si conviven `clip_inset` y /// `clip_ellipse`, gana la elipse (una sola capa de recorte por nodo). pub clip_ellipse: Option<[f32; 14]>, /// Si `Some((evenodd, puntos))`, recorta los descendientes a un POLÍGONO — /// modela `clip-path: polygon()`. Cada punto `[x_px, x_pct, y_px, y_pct]` /// resuelve `(x_px + x_pct/100·w, y_px + y_pct/100·h)` contra el rect. /// `evenodd` elige la regla de relleno. Implica clip aunque `clip == /// false`. Prioridad de recorte por nodo: polygon > elipse > inset > rect. pub clip_polygon: Option<(bool, Vec<[f32; 4]>)>, /// Si `Some((evenodd, d))`, recorta los descendientes a un PATH SVG — /// modela `clip-path: path()`. `d` es el string SVG crudo (user units px, /// relativos al origen del rect); el pintado lo parsea con /// `BezPath::from_svg` y lo traslada al origen del nodo. Si el parseo /// falla, no recorta. Implica clip aunque `clip == false`. Prioridad: /// path > polygon > elipse > inset > rect. pub clip_path_svg: Option<(bool, String)>, /// Si `Some([t,r,b,l])`, el clip-path se resuelve contra una caja de /// referencia (``) que es el rect del nodo ENCOGIDO por esos /// insets px (padding-box = border; content-box = border+padding). El /// pintado lo aplica ANTES de resolver la forma; sin forma, recorta a ese /// rect. `None` = referencia = border-box (rect completo). Fase 7.1225. pub clip_ref_inset: Option<[f32; 4]>, /// Msg a emitir cuando el cursor entra al rect del nodo (transición /// no-hover → hover). Útil para previews tipo "URL del link al /// pasar el mouse". pub on_pointer_enter: Option, /// Msg a emitir cuando el cursor sale del rect del nodo. pub on_pointer_leave: Option, /// Handler de **movimiento del cursor** sobre el nodo: recibe `(local_x, /// local_y, rect_w, rect_h)` en CADA `CursorMoved` mientras el cursor está /// encima (no sólo en la transición de entrada, a diferencia de /// [`Self::on_pointer_enter`]). Análogo posicional de hover, base de cosas /// como el thumbnail que sigue al cursor sobre un timeline o un drawer que /// reacciona a la posición. `None` no dispara update. pub on_pointer_move_at: Option>, /// Handler de rueda local. Si está presente y el cursor cae sobre este /// nodo, el runtime lo invoca antes del `App::on_wheel` global; un /// `Some(Msg)` consume el evento. Base de las áreas de scroll /// autocontenidas. Ver [`ScrollFn`]. pub on_scroll: Option>, /// Handler de gesto de **escala** (pinch-to-zoom). Si está presente y el /// gesto cae sobre este nodo (Ctrl+rueda en desktop, pinch de trackpad en /// macOS), el runtime lo invoca con el factor incremental + el punto focal /// local. Base del zoom de canvases. Ver [`ScaleFn`] y [`View::on_scale`]. pub on_scale: Option>, /// Handler de gesto de **rotación** (dos dedos en trackpad, macOS). Si /// está presente y el gesto cae sobre este nodo, el runtime lo invoca con /// el delta de ángulo incremental (radianes) + el punto focal local. Ver /// [`RotateFn`] y [`View::on_rotate`]. pub on_rotate: Option>, /// Msg a emitir en **doble-tap** (dos presses izquierdos sobre este nodo /// dentro de una ventana temporal corta y muy cerca). Es un evento /// **aditivo**: si el nodo también tiene `on_click`, éste igual dispara en /// cada press; el doble-tap llega además en el segundo. Para doble-tap /// exclusivo, poné el handler en un nodo sin `on_click`. Ver /// [`View::on_double_tap`]. pub on_double_tap: Option, /// Variante posicional de [`Self::on_double_tap`]: recibe la posición del /// segundo tap relativa al rect del nodo (para zoom-to-point, etc.). Gana /// sobre `on_double_tap` si ambos están. pub on_double_tap_at: Option>, /// Msg a emitir en **long-press** (mantener el botón izquierdo sobre este /// nodo ~500 ms sin moverse ni soltar). El runtime lo arbitra por tiempo: /// si el cursor se aleja (pasó a drag/scroll) o se suelta antes, se /// cancela. Evento **aditivo** (ver [`Self::on_double_tap`]); el caso /// limpio es un nodo con drag-to-pan + long-press y sin `on_click` (un /// canvas). Útil para menús contextuales táctiles / selección. Ver /// [`View::on_long_press`]. pub on_long_press: Option, /// Variante posicional de [`Self::on_long_press`]: recibe la posición del /// press relativa al rect del nodo (para abrir el menú en el punto). Gana /// sobre `on_long_press` si ambos están. pub on_long_press_at: Option>, /// Marca este nodo como **enfocable** con el id opaco `u64`. El runtime /// mantiene el foco (uno por ventana) y lo mueve con Tab/Shift+Tab en /// orden de árbol (pre-orden) y al clickear un nodo enfocable; notifica /// a la app vía `App::on_focus` para que pinte el ring y rutee el /// teclado. El id lo elige el caller (índice de campo, hash, etc.). pub focusable: Option, /// Marca este nodo de **texto** como seleccionable con el mouse fuera del /// editor (arrastrar resalta, Ctrl/Cmd+C copia). El `u64` es una **key /// estable** entre rebuilds del `View` (los `NodeId` de taffy cambian cada /// frame, así que la selección retenida en el runtime se ancla a esta key, /// igual que `animated`). Sólo tiene efecto en nodos con `text` uniforme /// (no `runs`/`spans`). Ver [`View::selectable`]. pub text_select_key: Option, /// Opacidad multiplicada sobre TODO el subtree (este nodo + hijos), /// en `[0.0, 1.0]`. Se realiza con `scene.push_layer(Mix::Normal, a, …)` /// alrededor del rect del nodo: el subárbol se rasteriza en una capa /// intermedia y se compone al alfa indicado contra lo que ya hay /// detrás. `None` = sin capa (caso de la abrumadora mayoría de /// nodos). Útil para fade-in/out de overlays, ghosts mientras se /// arrastra, modales que aparecen, panels "vidrio". Note que la /// composición tiene costo (allocate + blit), por lo que sólo /// poblar este slot cuando hace falta — no es un atributo gratis. pub alpha: Option, /// Animación **implícita** de las props de paint (fill/radius): cuando el /// valor cambia entre frames, el runtime interpola en vez de saltar. `None` /// = sin animación (la abrumadora mayoría). La `key` debe ser estable entre /// rebuilds. Ver [`Anim`] y [`View::animated`]. Lo consume el runtime vía /// [`AnimRegistry::reconcile`] (DESPUÉS de layout, ANTES de paint). pub anim: Option, /// **Animación implícita de tamaño** (Flutter `AnimatedSize` / /// Compose `animateContentSize()`). `None` = sin animación. La key /// debe ser estable entre rebuilds. A diferencia de [`Self::anim`] /// (props de paint, reconcilia DESPUÉS de layout), el tamaño tiene /// que estar firme **antes** del layout — siblings/hijos dependen /// del rect del nodo. El runtime llama /// [`reconcile_size_anim`] sobre el `View` tree **antes** de /// `mount` y parcha `style.size` con el valor interpolado. Sólo se /// activa si ambos `style.size.width` y `style.size.height` son /// `Dimension::Length(_)`. Ver [`SizeAnim`] y [`View::animated_size`]. pub animated_size: Option, /// **Semántica accesible** del nodo (rol, label, value, flags ARIA). El /// runtime la traduce a un árbol AccessKit por frame para alimentar /// lectores de pantalla (NVDA/VoiceOver/Orca/TalkBack). `None` = no /// declarada (el lector lee el texto plano si lo hay, sin rol específico). /// Ver [`SemanticsSpec`]. pub semantics: Option, /// **Hero shared-element**: marca este nodo como una identidad estable /// entre frames. Si la misma `key` aparece en otra posición en un frame /// siguiente, el runtime interpola `transform` para "volar" del rect /// anterior al actual durante la `duration` declarada. Ver /// [`Hero`] y [`HeroRegistry`]. `None` = sin hero (la abrumadora mayoría). pub hero: Option, /// Transformación afín 2D aplicada a este nodo y todo su subtree /// **alrededor del centro de su propio rect** (convención CSS /// `transform-origin: 50% 50%`). El runtime resuelve el centro en /// `paint` (sólo entonces conoce el layout computado) y compone /// `T(centro) · transform · T(-centro)` sobre la transformación /// acumulada del padre, así nodos anidados transforman en el espacio /// ya transformado de su ancestro — igual que CSS. `None` = identidad /// (la abrumadora mayoría de nodos). Pensado para `transform`/ /// `@keyframes` CSS de puriy (rotate/scale/translate). El hit-test /// **respeta** el afín (un nodo transformado recibe clicks donde se ve /// pintado). Limitación restante: los `painter`/`runs` custom no heredan /// el afín, y la posición local que reciben los handlers `*_at` se /// reporta en espacio de pantalla, no en el espacio local del nodo. pub transform: Option, /// Traslación RELATIVA al tamaño del propio nodo, en fracciones de su rect /// computado: `(fx, fy)` ⇒ desplaza `(fx · w, fy · h)` px. Se resuelve en /// `paint`/`hit_test` (única instancia donde se conoce el tamaño usado) y /// se compone como el factor más externo del afín del nodo, ANTES del /// centrado por `transform-origin`. Pensado para el `translate(<%>)` de CSS /// (p. ej. el truco de centrado `translate(-50%, -50%)` ⇒ `(-0.5, -0.5)`), /// que no es expresable como `Affine` fijo porque el % depende del layout. /// `None` = sin traslación relativa (la abrumadora mayoría). Compone con /// `transform` (afín fijo) si ambos están: `T_rel · transform`. pub transform_rel: Option<(f64, f64)>, /// Punto de pivote de `transform` (CSS `transform-origin`). `None` ⇒ el /// default CSS `50% 50%` (centro del rect) — el caso mayoritario. Ver /// [`TransformPivot`] y [`View::transform_origin`]. pub transform_origin: Option, /// Texto de **tooltip**: si está, el runtime/cliente puede mostrar un /// rótulo flotante cuando el cursor se posa sobre este nodo. Llimphi sólo /// transporta el dato hasta el [`MountedNode`]; *quién* lo pinta (un overlay /// del runtime, una surface popup del cliente) lo decide el consumidor. El /// hit-test de hover ya localiza el nodo bajo el cursor. `None` = sin tip. pub tooltip: Option, /// Forma del puntero del mouse mientras está sobre este nodo (o un /// descendiente sin cursor propio — se hereda del ancestro más cercano que /// lo declare). El runtime lo resuelve en el hit-test de hover y lo aplica a /// la ventana. `None` = hereda (default flecha en la raíz). Ver [`Cursor`] y /// [`View::cursor`]. Llimphi-native (sin winit); el runtime lo mapea. pub cursor: Option, /// Feedback de tap **ripple/InkWell**: al presionar este nodo, el runtime /// emite una salpicadura Material (círculo que se expande desde el punto y /// se desvanece, recortado al contorno del nodo). Es puro feedback visual, /// aditivo al `on_click`; vive en el runtime ([`RippleRegistry`]), no en el /// `Model`. `None` = sin ripple. Ver [`View::ripple`]. pub ripple: Option, /// Constructor **diferido** sensible al tamaño (`LayoutBuilder`). Si está /// presente, este nodo NO usa sus `children` estáticos: el runtime resuelve /// su slot en una primera pasada de layout y luego invoca esta closure con /// las [`Constraints`] resueltas para producir el subárbol. `None` = nodo /// normal (la abrumadora mayoría). Ver [`View::layout_builder`]. pub layout_builder: Option>, /// Backdrop blur sobre el contenido pintado **debajo** de este nodo. /// Ver [`View::backdrop_blur`] / [`MountedNode::backdrop_blur`]. v1: /// sólo se aplica a nodos top-level sin clip/alpha ancestral. pub backdrop_blur: Option, /// Filtros CSS (`filter: …`) sobre el propio subárbol del nodo. Vacío = sin /// filtro. Ver [`View::filter`] / [`FilterOp`]. Fase 7.1232. pub filter: Vec, /// **Modo de mezcla** del nodo entero contra su backdrop (CSS /// `mix-blend-mode`). `Some(bm)` ⇒ el subárbol del nodo se rasteriza en una /// capa aislada (`scene.push_layer(bm, …)` alrededor del rect) y se mezcla /// con el modo `bm` contra todo lo pintado antes en el stacking context. /// `None` = source-over normal (la abrumadora mayoría). Ver [`View::blend`]. /// Fase 7.1237. pub blend: Option, pub children: Vec>, } impl View { /// Transforma el `Msg` de **todo el árbol** vía `f`, devolviendo /// `View`. Es la pieza que permite **embeber el `view` de un sub-app** /// en un host (junto con [`crate::Handle::lift`] para sus efectos): el host /// pinta `sub_view.map(Msg::Sub)` y los eventos del sub-árbol vuelven como /// su propio `Msg`. Patrón estándar de anidado Elm. `f` se comparte (`Arc`) /// entre todos los callbacks e hijos, así que debe ser `Send + Sync`. pub fn map(self, f: F) -> View where Msg2: 'static, F: Fn(Msg) -> Msg2 + Send + Sync + 'static, { self.map_shared(Arc::new(f)) } fn map_shared( self, f: Arc Msg2 + Send + Sync>, ) -> View { let View { style, fill, hover_fill, radius, corner_radii, shadow, fill_gradient, border, text, image, image_fit, mask_image, mask_placement, mask_extra, painter, gpu_painter, over_painter, on_click, on_click_at, on_right_click, on_right_click_at, on_middle_click, drag, drag_at, drag_velocity, drag_payload, on_drop, drop_hover_fill, clip, clip_inset, clip_ellipse, clip_polygon, clip_path_svg, clip_ref_inset, on_pointer_enter, on_pointer_leave, on_pointer_move_at, on_scroll, on_scale, on_rotate, on_double_tap, on_double_tap_at, on_long_press, on_long_press_at, focusable, text_select_key, alpha, anim, animated_size, semantics, hero, transform, transform_rel, transform_origin, tooltip, cursor, ripple, layout_builder, backdrop_blur, filter, blend, children, } = self; // Wrappers: cada callback que produce `Option` se reenvía y su // resultado se eleva con `f`. `f` se clona por callback (todos comparten // el mismo `Arc`). View { // — campos agnósticos al Msg: pasan tal cual — style, fill, hover_fill, radius, corner_radii, shadow, fill_gradient, border, text, image, image_fit, mask_image, mask_placement, mask_extra, painter, gpu_painter, over_painter, drag_payload, drop_hover_fill, clip, clip_inset, clip_ellipse, clip_polygon, clip_path_svg, clip_ref_inset, focusable, text_select_key, alpha, anim, animated_size, semantics, hero, transform, transform_rel, transform_origin, tooltip, cursor, ripple, backdrop_blur, filter, blend, // — Msg simples — on_click: on_click.map(|m| f(m)), on_right_click: on_right_click.map(|m| f(m)), on_middle_click: on_middle_click.map(|m| f(m)), on_pointer_enter: on_pointer_enter.map(|m| f(m)), on_pointer_leave: on_pointer_leave.map(|m| f(m)), on_double_tap: on_double_tap.map(|m| f(m)), on_long_press: on_long_press.map(|m| f(m)), // — ClickAtFn (lx, ly, w, h) — on_click_at: on_click_at.map(|h| { let f = f.clone(); Arc::new(move |a, b, c, d| h(a, b, c, d).map(|m| f(m))) as ClickAtFn }), on_right_click_at: on_right_click_at.map(|h| { let f = f.clone(); Arc::new(move |a, b, c, d| h(a, b, c, d).map(|m| f(m))) as ClickAtFn }), on_pointer_move_at: on_pointer_move_at.map(|h| { let f = f.clone(); Arc::new(move |a, b, c, d| h(a, b, c, d).map(|m| f(m))) as ClickAtFn }), on_double_tap_at: on_double_tap_at.map(|h| { let f = f.clone(); Arc::new(move |a, b, c, d| h(a, b, c, d).map(|m| f(m))) as ClickAtFn }), on_long_press_at: on_long_press_at.map(|h| { let f = f.clone(); Arc::new(move |a, b, c, d| h(a, b, c, d).map(|m| f(m))) as ClickAtFn }), // — drag / scroll / gestos — drag: drag.map(|h| { let f = f.clone(); Arc::new(move |p, dx, dy| h(p, dx, dy).map(|m| f(m))) as DragFn }), drag_at: drag_at.map(|h| { let f = f.clone(); Arc::new(move |p, dx, dy, lx, ly| h(p, dx, dy, lx, ly).map(|m| f(m))) as DragAtFn }), drag_velocity: drag_velocity.map(|h| { let f = f.clone(); Arc::new(move |p, dx, dy, vx, vy| h(p, dx, dy, vx, vy).map(|m| f(m))) as DragVelocityFn }), on_drop: on_drop.map(|h| { let f = f.clone(); Arc::new(move |payload| h(payload).map(|m| f(m))) as DropFn }), on_scroll: on_scroll.map(|h| { let f = f.clone(); Arc::new(move |dx, dy| h(dx, dy).map(|m| f(m))) as ScrollFn }), on_scale: on_scale.map(|h| { let f = f.clone(); Arc::new(move |ph, s, cx, cy| h(ph, s, cx, cy).map(|m| f(m))) as ScaleFn }), on_rotate: on_rotate.map(|h| { let f = f.clone(); Arc::new(move |ph, r, cx, cy| h(ph, r, cx, cy).map(|m| f(m))) as RotateFn }), // — layout_builder produce un View: recursá el map — layout_builder: layout_builder.map(|h| { let f = f.clone(); Arc::new(move |c| h(c).map_shared(f.clone())) as LayoutBuilderFn }), // — hijos: recursión — children: children .into_iter() .map(|c| c.map_shared(f.clone())) .collect(), } } } /// Versión "instalada" del árbol: cada nodo tiene su NodeId de taffy, color /// y handler. Se mantiene en orden de inserción (recorrido pre-orden), así /// el hit-test puede iterar al revés para honrar el orden de pintado. /// /// `pub` (con campos `pub`) porque el runtime (llimphi-ui) lee el árbol /// montado para hit-test y para la pasada GPU directa, pero vive en otro /// crate. No se construye fuera de [`mount`]. pub struct Mounted { pub root: NodeId, pub nodes: Vec>, /// Contenido de texto por nodo-hoja, para que el runtime lo mida con /// parley durante `compute_with_measure` y taffy reserve el alto real /// del texto envuelto (varias líneas) en vez de una sola. Sin esto un /// párrafo que envuelve a N líneas se aplastaría en la altura de una /// (el bug clásico de "textos aplastados"). Sólo se pueblan hojas con /// texto uniforme (sin `runs` multicolor, que el caller dimensiona). pub text_measures: HashMap, } /// Datos de un nodo-hoja de texto necesarios para medirlo (shaping + /// line-break) sin volver a tocar el `View`. Lo consume el runtime en la /// función de medición que le pasa a [`LayoutTree::compute_with_measure`]. #[derive(Clone)] pub struct TextMeasure { pub content: String, pub size_px: f32, pub alignment: llimphi_text::Alignment, pub italic: bool, pub font_family: Option, pub line_height: f32, pub weight: f32, pub max_lines: Option, pub ellipsis: bool, /// Idem [`TextSpec::underline`]. Se replica en la medida porque parley /// no cambia de ancho con decoración (no toca el shaping); pero la clave /// del caché de shaping sí cambia, y queremos que medida y pintado /// peguen la misma entrada del caché. pub underline: bool, /// Idem [`TextSpec::strikethrough`]. Mismo razonamiento que `underline`. pub strikethrough: bool, /// Idem [`TextSpec::spans`]. La medida usa el mismo /// `Typesetter::layout_spans` que el pintado, así taffy reserva el alto /// real considerando overrides de `size_px` por span (un `

` inline /// dentro de un párrafo agranda esa línea). `None`/`vacío` = medir con /// `layout_clamped` (camino uniforme). pub spans: Option>, /// Idem [`TextSpec::letter_spacing`]. Entra en la medida porque cambia el /// ancho del shaping (y la clave del caché). pub letter_spacing: f32, /// Idem [`TextSpec::word_spacing`]. Mismo razonamiento que `letter_spacing`. pub word_spacing: f32, /// Idem [`TextSpec::no_wrap`]. Entra en la medida porque cambia el ancho /// reservado: con `no_wrap` el texto se mide en una sola línea (ancho /// completo) en vez de envolver al `available`. pub no_wrap: bool, /// Idem [`TextSpec::overflow_wrap`]. Entra en la medida porque parte la /// palabra larga: con el flag, el ancho mínimo del bloque deja de estar /// fijado por el token más ancho. pub overflow_wrap: bool, } /// Cómo encajar una imagen en el rect del nodo (CSS `object-fit` / /// Flutter `BoxFit`). El runtime calcula la escala y el origen /// correspondientes a esta política y siempre recorta al /// `node_rrect` del nodo, así el clip respeta `radius` / /// `corner_radii`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ImageFit { /// Preservar aspect ratio, **caber** dentro del rect (escala = /// `min(sx, sy)`). Deja banda en el eje menos restrictivo. /// CSS `object-fit: contain` / Flutter `BoxFit.contain`. **Default /// histórico** — lo que hacía `View::image()` antes del Bloque 12. Contain, /// Preservar aspect ratio, **cubrir** todo el rect (escala = /// `max(sx, sy)`). Recorta el sobrante en el eje menos /// restrictivo (el clip al `node_rrect` lo absorbe). CSS /// `object-fit: cover` / Flutter `BoxFit.cover` — ideal para /// avatares y hero images. Cover, /// Estirar la imagen para ocupar el rect, **sin** preservar /// aspect ratio (`sx`/`sy` independientes). CSS `object-fit: /// fill` / Flutter `BoxFit.fill`. Fill, /// **No** escalar la imagen — pintarla a su tamaño original, /// centrada en el rect. Si la imagen excede el rect, el clip al /// `node_rrect` la recorta. CSS `object-fit: none` / Flutter /// `BoxFit.none`. None, } /// Longitud de un eje de [`MaskSize`]/posición de máscara, **sin resolver** — /// el paint la resuelve contra el rect del nodo. Neutral respecto de CSS: el /// frontend (p. ej. puriy) traduce `mask-size`/`mask-position` a esto. Fase /// 7.1227. #[derive(Debug, Clone, Copy, PartialEq)] pub enum MaskLen { /// Tamaño intrínseco de la imagen (en size) / offset 0 (en position). Auto, /// Longitud absoluta en px. Px(f32), /// Porcentaje: en size, del lado correspondiente del rect; en position, /// alineación CSS (el `p%` de la máscara cae sobre el `p%` del rect). Pct(f32), } /// `mask-size` neutral (espejo de `BackgroundSize`). Ver [`MaskPlacement`]. /// Fase 7.1227. #[derive(Debug, Clone, Copy, PartialEq)] pub enum MaskSize { /// Tamaño intrínseco de la imagen-máscara. Auto, /// Escalar preservando aspecto hasta **cubrir** el rect. Cover, /// Escalar preservando aspecto hasta **caber** en el rect. Contain, /// Tamaño explícito por eje (`Auto` en un eje = derivar por aspecto). Explicit { x: MaskLen, y: MaskLen }, } /// Modo de una máscara (CSS `mask-mode`). Decide qué canal del píxel-máscara /// modula el alpha del contenido. Fase 7.1228. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum MaskMode { /// La **luminancia** del píxel multiplica el alpha (negro = oculto, blanco = /// visible). Lo usa CSS para máscaras SVG ``. Es el default del /// compositor cuando no hay `MaskPlacement` (camino estirado, Fase 7.1226). #[default] Luminance, /// El **canal alpha** del píxel modula el alpha (transparente = oculto). Es /// el default CSS para imágenes raster (`mask-mode: match-source`). Se pinta /// con `Compose::DestIn` en vez de la capa de luminancia. Alpha, } /// Operador de combinación entre capas de máscara (CSS `mask-composite`). Mapea /// a un `Compose` Porter-Duff de vello cuando una capa extra se compone sobre /// las de abajo. Fase 7.1231. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum MaskCompose { /// La capa se **suma** sobre las de abajo (source-over). Default CSS. #[default] Add, /// La capa **resta** (source-out: la fuente donde NO solapa el destino). Subtract, /// **Intersección** (source-in: la fuente donde solapa el destino). Intersect, /// **Exclusión** (xor: las regiones no solapadas de ambas). Exclude, } /// Encaje y modo de una **máscara** (CSS `mask-size` + `mask-position` + /// `mask-repeat` + `mask-mode`), resuelto contra el rect del nodo en el paint, /// con la misma aritmética que `background-image`. En el [`MountedNode`] viaja /// como `Option`: `None` = estirar la máscara al border-box en modo luminancia /// (comportamiento de la Fase 7.1226). Fase 7.1227 (encaje), 7.1228 (modo). #[derive(Debug, Clone, Copy, PartialEq)] pub struct MaskPlacement { /// Tamaño del tile. pub size: MaskSize, /// Offset/alineación horizontal del primer tile. pub pos_x: MaskLen, /// Offset/alineación vertical del primer tile. pub pos_y: MaskLen, /// Tilear en X (`mask-repeat` cubre el eje horizontal). pub repeat_x: bool, /// Tilear en Y. pub repeat_y: bool, /// Canal que modula el alpha (luminancia vs alpha). Fase 7.1228. pub mode: MaskMode, /// Insets `[top, right, bottom, left]` px del border-box a la caja de /// `mask-clip`: el efecto de la máscara se **recorta** a esa caja. `None` = /// border-box. Fase 7.1230. pub clip_inset: Option<[f32; 4]>, /// Insets `[top, right, bottom, left]` px del border-box a la caja de /// `mask-origin`: size/position/tiling se resuelven contra esa caja. `None` /// = border-box. Fase 7.1230. pub origin_inset: Option<[f32; 4]>, } impl Default for ImageFit { fn default() -> Self { ImageFit::Contain } } /// Forma del puntero del mouse. Subconjunto práctico, llimphi-native (el /// compositor no depende de winit). El runtime (`llimphi-ui`) mapea 1:1 a /// `winit::window::CursorIcon`. Nombres alineados con CSS/winit. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Cursor { /// Flecha por defecto. Default, /// Manito — sobre algo clickeable (links, botones). Pointer, /// I-beam — sobre texto editable/seleccionable. Text, /// Cruz — selección precisa (canvas, picker de color). Crosshair, /// Cuatro flechas — mover un objeto. Move, /// Mano abierta — agarrable (antes de arrastrar). Grab, /// Mano cerrada — arrastrando. Grabbing, /// Prohibido — drop no permitido / acción inválida. NotAllowed, /// Reloj/espera — operación bloqueante. Wait, /// Progreso — ocupado pero la UI responde. Progress, /// Interrogación — ayuda contextual. Help, /// Resize horizontal (columna / divisor vertical). ColResize, /// Resize vertical (fila / divisor horizontal). RowResize, /// Resize este-oeste. EwResize, /// Resize norte-sur. NsResize, /// Resize diagonal ↗↙. NeswResize, /// Resize diagonal ↖↘. NwseResize, /// Lupa + (zoom in). ZoomIn, /// Lupa − (zoom out). ZoomOut, } pub struct MountedNode { pub id: NodeId, pub fill: Option, pub hover_fill: Option, pub radius: f64, pub corner_radii: Option, pub shadow: Option, pub fill_gradient: Option, pub border: Option, pub text: Option, pub image: Option, /// Política de encaje de [`Self::image`] (ver [`ImageFit`]). `None` /// = `Contain`. pub image_fit: Option, /// Máscara de luminancia del subárbol (CSS `mask-image`). Ver /// [`View::mask_image`]. El paint aísla el subárbol y aplica la luminancia /// de esta imagen como alpha. `None` = sin máscara. pub mask_image: Option, /// Encaje de [`Self::mask_image`] (size/position/repeat). `None` = estirar /// al border-box. Ver [`MaskPlacement`]. Fase 7.1227. pub mask_placement: Option, /// Capas de máscara adicionales `(imagen, operador)` (ver [`View::mask_extra`]). /// Comparten el `mask_placement` con la capa 0. Fase 7.1231. pub mask_extra: Vec<(Image, MaskCompose)>, pub painter: Option, pub gpu_painter: Option, pub over_painter: Option, pub on_click: Option, pub on_click_at: Option>, pub on_right_click: Option, pub on_right_click_at: Option>, pub on_middle_click: Option, pub drag: Option>, pub drag_at: Option>, pub drag_velocity: Option>, pub drag_payload: Option, pub on_drop: Option>, pub drop_hover_fill: Option, pub clip: bool, pub clip_inset: Option<[f32; 4]>, pub clip_ellipse: Option<[f32; 14]>, pub clip_polygon: Option<(bool, Vec<[f32; 4]>)>, pub clip_path_svg: Option<(bool, String)>, pub clip_ref_inset: Option<[f32; 4]>, pub on_pointer_enter: Option, pub on_pointer_leave: Option, pub on_pointer_move_at: Option>, pub on_scroll: Option>, /// Handler de gesto de escala (pinch-to-zoom) de este nodo. Ver /// [`View::on_scale`] y [`ScaleFn`]. pub on_scale: Option>, /// Handler de gesto de rotación (trackpad) de este nodo. Ver /// [`View::on_rotate`] y [`RotateFn`]. pub on_rotate: Option>, /// Handlers de doble-tap (ver [`View::on_double_tap`]). pub on_double_tap: Option, pub on_double_tap_at: Option>, /// Handlers de long-press (ver [`View::on_long_press`]). pub on_long_press: Option, pub on_long_press_at: Option>, pub focusable: Option, /// Key estable de selección de texto (ver [`View::selectable`]). pub text_select_key: Option, pub alpha: Option, pub anim: Option, /// Animación implícita de tamaño (ver [`View::animated_size`]). El /// runtime ya parchó `style.size` antes del layout — este campo se /// guarda principalmente para inspección/tests. pub animated_size: Option, /// Semántica accesible del nodo (ver [`View::semantics`]). El runtime la /// lee en cada paint para reconstruir el árbol AccessKit del frame. pub semantics: Option, /// Marca de hero shared-element (ver [`View::hero`]). El runtime lo lee /// en [`HeroRegistry::reconcile`] para enlazar identidad entre frames y /// escribir `transform` con la afín "fly" cuando el rect cambia. pub hero: Option, /// Transformación afín 2D del nodo (alrededor del centro de su rect). /// Ver [`View::transform`]. `paint` la compone con la del padre. pub transform: Option, /// Traslación relativa al tamaño del nodo (fracciones de su rect). Ver /// [`View::transform_rel`]. `paint`/`hit_test` la resuelven contra el rect. pub transform_rel: Option<(f64, f64)>, /// Pivote de `transform` (CSS `transform-origin`). `None` ⇒ centro. Ver /// [`TransformPivot`] / [`View::transform_origin`]. pub transform_origin: Option, /// Texto de tooltip de este nodo (ver [`View::tooltip`]). El consumidor lo /// lee tras un hit-test de hover para pintar el rótulo flotante. pub tooltip: Option, /// Forma del puntero sobre este nodo (ver [`View::cursor`]). El runtime la /// resuelve heredando del ancestro más cercano que la declare. pub cursor: Option, /// Ripple/InkWell de este nodo (ver [`View::ripple`]). El runtime lo /// dispara en el press y lo pinta vía [`RippleRegistry`]. pub ripple: Option, /// `true` si este nodo era un [`View::layout_builder`] (constructor diferido) /// al montarse. El runtime lo usa tras la primera pasada de layout para leer /// el rect del slot (vía [`collect_builder_constraints`]) e invocar la /// closure. Tras expandirse, el nodo final ya es normal (`false`). pub is_layout_builder: bool, /// **Backdrop blur** (CSS `backdrop-filter: blur(N)` / Flutter /// `BackdropFilter`). Sigma del Gauss en pixels; el runtime aplica una /// pasada separable (H+V) sobre la intermediate restringida al rect del /// nodo, **antes** de pintar el subárbol del nodo. El subárbol se compone /// sobre el backdrop ya borroso vía un buffer secundario. `None` = sin /// blur (la abrumadora mayoría). Limitación v1: el nodo no debe estar /// dentro de un ancestro con clip/alpha (los subárboles separados pintan /// fuera de esas capas — documentado en `PARIDAD-FLUTTER.md` Bloque 11). pub backdrop_blur: Option, /// Filtros CSS (`filter: …`) sobre el propio subárbol (ver [`View::filter`] /// / [`FilterOp`]). El runtime los recolecta con [`collect_filters`] y los /// aplica como post-pasada GPU sobre la intermediate, restringidos al rect /// del nodo, **después** de la rasterización. Vacío = sin filtro. Fase /// 7.1232. pub filter: Vec, /// Modo de mezcla del nodo entero contra su backdrop (CSS `mix-blend-mode`). /// Ver [`View::blend`] / [`MountedNode`]. `paint_range` abre una capa de /// blend (`push_layer(bm, …)`) alrededor del rect del nodo que envuelve /// fill + contenido + hijos y se cierra al fin del subárbol, mezclando el /// resultado contra lo ya pintado. `None` = source-over. Fase 7.1237. pub blend: Option, /// Índice (exclusivo) del fin del subárbol en `Mounted::nodes`. Los /// descendientes ocupan `[idx + 1, subtree_end)`. Hace de "barrera" en /// paint/hit_test para `pop_layer` y para saltar subárboles enteros. pub subtree_end: usize, }