pub struct View<Msg> {Show 62 fields
pub style: Style,
pub fill: Option<AlphaColor<Srgb>>,
pub hover_fill: Option<AlphaColor<Srgb>>,
pub radius: f64,
pub corner_radii: Option<RoundedRectRadii>,
pub shadow: Option<Shadow>,
pub fill_gradient: Option<Gradient>,
pub border: Option<Border>,
pub text: Option<TextSpec>,
pub image: Option<ImageBrush>,
pub image_fit: Option<ImageFit>,
pub mask_image: Option<ImageBrush>,
pub mask_placement: Option<MaskPlacement>,
pub mask_extra: Vec<(ImageBrush, MaskCompose)>,
pub painter: Option<Arc<dyn Fn(&mut Scene, &mut Typesetter, PaintRect) + Send + Sync>>,
pub gpu_painter: Option<Arc<dyn Fn(&Device, &Queue, &mut CommandEncoder, &TextureView, PaintRect, (u32, u32)) + Send + Sync>>,
pub over_painter: Option<Arc<dyn Fn(&mut Scene, &mut Typesetter, PaintRect) + Send + Sync>>,
pub on_click: Option<Msg>,
pub on_click_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_right_click: Option<Msg>,
pub on_right_click_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_middle_click: Option<Msg>,
pub drag: Option<Arc<dyn Fn(DragPhase, f32, f32) -> Option<Msg> + Send + Sync>>,
pub drag_at: Option<Arc<dyn Fn(DragPhase, f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub drag_velocity: Option<Arc<dyn Fn(DragPhase, f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub drag_payload: Option<u64>,
pub on_drop: Option<Arc<dyn Fn(u64) -> Option<Msg> + Send + Sync>>,
pub drop_hover_fill: Option<AlphaColor<Srgb>>,
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<Msg>,
pub on_pointer_leave: Option<Msg>,
pub on_pointer_move_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_scroll: Option<Arc<dyn Fn(f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_scale: Option<Arc<dyn Fn(GesturePhase, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_rotate: Option<Arc<dyn Fn(GesturePhase, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_double_tap: Option<Msg>,
pub on_double_tap_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub on_long_press: Option<Msg>,
pub on_long_press_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>,
pub focusable: Option<u64>,
pub text_select_key: Option<u64>,
pub alpha: Option<f32>,
pub anim: Option<Anim>,
pub animated_size: Option<SizeAnim>,
pub semantics: Option<SemanticsSpec>,
pub hero: Option<Hero>,
pub transform: Option<Affine>,
pub transform_rel: Option<(f64, f64)>,
pub transform_origin: Option<TransformPivot>,
pub tooltip: Option<String>,
pub cursor: Option<Cursor>,
pub ripple: Option<Ripple>,
pub layout_builder: Option<Arc<dyn Fn(Constraints) -> View<Msg> + Send + Sync>>,
pub backdrop_blur: Option<f32>,
pub filter: Vec<FilterOp>,
pub blend: Option<BlendMode>,
pub children: Vec<View<Msg>>,
}Expand description
Nodo de la vista declarativa. Estilo de layout (taffy) + relleno opcional (vello) + texto opcional (skrifa+vello) + Msg al click opcional + hijos.
Fields§
§style: Style§fill: Option<AlphaColor<Srgb>>§hover_fill: Option<AlphaColor<Srgb>>Relleno cuando el cursor está sobre este nodo. Sin valor (None)
= no se reacciona al hover.
radius: f64§corner_radii: Option<RoundedRectRadii>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.
shadow: Option<Shadow>Sombra proyectada detrás del nodo (drop shadow). None = sin sombra
(la mayoría de nodos). Ver Shadow.
fill_gradient: Option<Gradient>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.
border: Option<Border>Borde (stroke) sobre el contorno redondeado. Ver Border.
text: Option<TextSpec>§image: Option<ImageBrush>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).
image_fit: Option<ImageFit>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.
mask_image: Option<ImageBrush>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.
mask_placement: Option<MaskPlacement>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.
mask_extra: Vec<(ImageBrush, MaskCompose)>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.
painter: Option<Arc<dyn Fn(&mut Scene, &mut Typesetter, PaintRect) + Send + Sync>>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.
gpu_painter: Option<Arc<dyn Fn(&Device, &Queue, &mut CommandEncoder, &TextureView, PaintRect, (u32, u32)) + Send + Sync>>Pintor GPU directo. Se invoca DESPUÉS de la pasada vello del
frame; comparte tree y orden DFS con los demás. Ver
GpuPaintFn.
over_painter: Option<Arc<dyn Fn(&mut Scene, &mut Typesetter, PaintRect) + Send + Sync>>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).
on_click: Option<Msg>§on_click_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
on_right_click: Option<Msg>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ú.
on_right_click_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
on_middle_click: Option<Msg>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.
drag: Option<Arc<dyn Fn(DragPhase, f32, f32) -> Option<Msg> + Send + Sync>>Handler de drag. Si está presente, este nodo arrastra (y NO emite
on_click al presionar — un nodo es uno u otro).
drag_at: Option<Arc<dyn Fn(DragPhase, f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>Variante de drag que recibe la posición inicial del press relativa
al rect del nodo. Gana sobre drag si ambos están presentes.
drag_velocity: Option<Arc<dyn Fn(DragPhase, f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>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]).
drag_payload: Option<u64>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.
on_drop: Option<Arc<dyn Fn(u64) -> Option<Msg> + Send + Sync>>Handler invocado al soltar un drag sobre este nodo (drop target).
drop_hover_fill: Option<AlphaColor<Srgb>>Color a pintar mientras un drag activo está hovereando este drop
target. Sobrepone a fill/hover_fill cuando aplica.
clip: boolSi 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.
clip_inset: Option<[f32; 4]>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.
clip_ellipse: Option<[f32; 14]>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).
clip_polygon: Option<(bool, Vec<[f32; 4]>)>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.
clip_path_svg: Option<(bool, String)>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.
clip_ref_inset: Option<[f32; 4]>Si Some([t,r,b,l]), el clip-path se resuelve contra una caja de
referencia (<geometry-box>) 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.
on_pointer_enter: Option<Msg>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”.
on_pointer_leave: Option<Msg>Msg a emitir cuando el cursor sale del rect del nodo.
on_pointer_move_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
on_scroll: Option<Arc<dyn Fn(f32, f32) -> Option<Msg> + Send + Sync>>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.
on_scale: Option<Arc<dyn Fn(GesturePhase, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
on_rotate: Option<Arc<dyn Fn(GesturePhase, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
on_double_tap: Option<Msg>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.
on_double_tap_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
on_long_press: Option<Msg>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.
on_long_press_at: Option<Arc<dyn Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync>>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.
focusable: Option<u64>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.).
text_select_key: Option<u64>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.
alpha: Option<f32>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.
anim: Option<Anim>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).
animated_size: Option<SizeAnim>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.
semantics: Option<SemanticsSpec>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.
hero: Option<Hero>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).
transform: Option<Affine>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.
transform_rel: Option<(f64, f64)>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.
transform_origin: Option<TransformPivot>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.
tooltip: Option<String>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.
cursor: Option<Cursor>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.
ripple: Option<Ripple>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.
layout_builder: Option<Arc<dyn Fn(Constraints) -> View<Msg> + Send + Sync>>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.
backdrop_blur: Option<f32>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.
filter: Vec<FilterOp>Filtros CSS (filter: …) sobre el propio subárbol del nodo. Vacío = sin
filtro. Ver View::filter / FilterOp. Fase 7.1232.
blend: Option<BlendMode>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.
children: Vec<View<Msg>>Implementations§
Source§impl<Msg> View<Msg>
impl<Msg> View<Msg>
Sourcepub fn new(style: Style) -> View<Msg>
pub fn new(style: Style) -> View<Msg>
Examples found in repository?
70 fn view(model: &Self::Model) -> View<Self::Msg> {
71 let body_text = if model.is_empty() {
72 "tipea algo · ctrl+L limpia · enter salto · backspace borra".to_string()
73 } else {
74 // Cursor visual al final del contenido.
75 format!("{model}\u{2588}")
76 };
77 let body_color = if model.is_empty() {
78 Color::from_rgba8(110, 130, 150, 255)
79 } else {
80 Color::from_rgba8(220, 230, 240, 255)
81 };
82
83 let body = View::new(Style {
84 size: Size {
85 width: percent(1.0_f32),
86 height: percent(1.0_f32),
87 },
88 flex_grow: 1.0,
89 ..Default::default()
90 })
91 .text_aligned(body_text, 22.0, body_color, Alignment::Start);
92
93 let status = View::new(Style {
94 size: Size {
95 width: percent(1.0_f32),
96 height: length(36.0_f32),
97 },
98 ..Default::default()
99 })
100 .fill(Color::from_rgba8(30, 36, 48, 255))
101 .text(
102 format!("{} chars", model.chars().count()),
103 16.0,
104 Color::from_rgba8(160, 180, 200, 255),
105 );
106
107 View::new(Style {
108 flex_direction: FlexDirection::Column,
109 size: Size {
110 width: percent(1.0_f32),
111 height: percent(1.0_f32),
112 },
113 gap: Size {
114 width: length(0.0_f32),
115 height: length(8.0_f32),
116 },
117 padding: llimphi_ui::llimphi_layout::taffy::Rect {
118 left: length(24.0_f32),
119 right: length(24.0_f32),
120 top: length(24.0_f32),
121 bottom: length(24.0_f32),
122 },
123 ..Default::default()
124 })
125 .fill(Color::from_rgba8(20, 24, 32, 255))
126 .children(vec![body, status])
127 }More examples
40 fn view(_: &Self::Model) -> View<Self::Msg> {
41 let mut children: Vec<View<()>> = vec![View::new(Style {
42 size: Size {
43 width: percent(1.0_f32),
44 height: Dimension::auto(),
45 },
46 ..Default::default()
47 })
48 .text_aligned(
49 "Texto seleccionable (arrastrá + Ctrl/Cmd+C)",
50 22.0,
51 Color::from_rgba8(230, 240, 250, 255),
52 Alignment::Start,
53 )];
54
55 for (i, p) in PARRAFOS.iter().enumerate() {
56 children.push(
57 View::new(Style {
58 size: Size {
59 width: percent(1.0_f32),
60 height: Dimension::auto(),
61 },
62 ..Default::default()
63 })
64 .text_aligned(
65 *p,
66 16.0,
67 Color::from_rgba8(205, 214, 226, 255),
68 Alignment::Start,
69 )
70 // La línea clave: cada párrafo es seleccionable con una key
71 // estable (su índice). El resaltado + copy los hace el runtime.
72 .selectable(i as u64),
73 );
74 }
75
76 View::new(Style {
77 flex_direction: FlexDirection::Column,
78 size: Size {
79 width: percent(1.0_f32),
80 height: percent(1.0_f32),
81 },
82 gap: Size {
83 width: length(0.0_f32),
84 height: length(20.0_f32),
85 },
86 padding: Rect {
87 left: length(40.0_f32),
88 right: length(40.0_f32),
89 top: length(36.0_f32),
90 bottom: length(36.0_f32),
91 },
92 align_items: Some(AlignItems::Start),
93 ..Default::default()
94 })
95 .fill(Color::from_rgba8(20, 24, 32, 255))
96 .children(children)
97 }54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }43 fn view(model: &Self::Model) -> View<Self::Msg> {
44 let number = View::new(Style {
45 size: Size {
46 width: percent(1.0_f32),
47 height: Dimension::auto(),
48 },
49 flex_grow: 1.0,
50 align_items: Some(AlignItems::Center),
51 justify_content: Some(JustifyContent::Center),
52 ..Default::default()
53 })
54 .text(model.to_string(), 160.0, Color::from_rgba8(230, 240, 250, 255));
55
56 let increment = View::new(Style {
57 size: Size {
58 width: length(160.0_f32),
59 height: length(56.0_f32),
60 },
61 align_items: Some(AlignItems::Center),
62 justify_content: Some(JustifyContent::Center),
63 ..Default::default()
64 })
65 .fill(Color::from_rgba8(60, 200, 130, 255))
66 .radius(12.0)
67 .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
68 .on_click(Msg::Increment);
69
70 let reset = View::new(Style {
71 size: Size {
72 width: length(120.0_f32),
73 height: length(56.0_f32),
74 },
75 align_items: Some(AlignItems::Center),
76 justify_content: Some(JustifyContent::Center),
77 ..Default::default()
78 })
79 .fill(Color::from_rgba8(220, 80, 80, 255))
80 .radius(12.0)
81 .text("reset", 22.0, Color::from_rgba8(30, 10, 10, 255))
82 .on_click(Msg::Reset);
83
84 let buttons = View::new(Style {
85 flex_direction: FlexDirection::Row,
86 size: Size {
87 width: percent(1.0_f32),
88 height: length(56.0_f32),
89 },
90 gap: Size {
91 width: length(16.0_f32),
92 height: length(0.0_f32),
93 },
94 justify_content: Some(JustifyContent::Center),
95 ..Default::default()
96 })
97 .children(vec![increment, reset]);
98
99 View::new(Style {
100 flex_direction: FlexDirection::Column,
101 size: Size {
102 width: percent(1.0_f32),
103 height: percent(1.0_f32),
104 },
105 gap: Size {
106 width: length(0.0_f32),
107 height: length(24.0_f32),
108 },
109 padding: llimphi_ui::llimphi_layout::taffy::Rect {
110 left: length(32.0_f32),
111 right: length(32.0_f32),
112 top: length(32.0_f32),
113 bottom: length(32.0_f32),
114 },
115 ..Default::default()
116 })
117 .fill(Color::from_rgba8(20, 24, 32, 255))
118 .children(vec![number, buttons])
119 }101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn backdrop_blur(self, sigma: f32) -> View<Msg>
pub fn backdrop_blur(self, sigma: f32) -> View<Msg>
Aplica un backdrop blur Gaussiano al contenido pintado debajo
de este nodo, restringido al rect del nodo (CSS backdrop-filter: blur(N) / Flutter BackdropFilter). El runtime descompone el árbol
en “fondo + subárbol del nodo”, renderiza el fondo a la intermediate,
borronea el rect con un Gauss separable, y compone el subárbol del
nodo sobre el backdrop borroso vía un buffer secundario. Útil para
chrome translúcido: sidebars/topbars con “vidrio esmerilado”.
sigma (pixels) controla el ancho del kernel — 4.0 “frosted glass”
suave; 8.0–16.0 un blur fuerte; >20 se ve apagado. v1 capa el
radius efectivo a 32 pixels (sigma > 10 empieza a clipear cola).
Limitación v1: sólo nodos top-level (children directos del root o
de un agrupador sin clip/alpha) renderizan correctamente — un
nodo dentro de una capa clippeada se pinta SIN clip en su pase, por
el reset de layer-stack al cambiar de scene. Documentado en
PARIDAD-FLUTTER.md Bloque 11.
Sourcepub fn filter(self, ops: Vec<FilterOp>) -> View<Msg>
pub fn filter(self, ops: Vec<FilterOp>) -> View<Msg>
Aplica una lista de filtros CSS (filter: …) al propio subárbol
del nodo (a diferencia de Self::backdrop_blur, que afecta lo pintado
debajo). El runtime los recolecta con collect_filters y los aplica
como post-pasada GPU sobre la intermediate, restringidos al rect del
nodo, en el orden de la lista. Hoy sólo se modela blur (FilterOp);
la lista crece por fase. Es ortogonal a clip/mask (un nodo puede
llevar todos). Lista vacía = sin filtro. Fase 7.1232.
Limitación v1 (igual que backdrop_blur): la post-pasada opera sobre
los píxeles finales del rect, así que no aísla el subárbol del fondo que
asome detrás. Adecuado para nodos opacos.
Sourcepub fn blend(self, bm: BlendMode) -> View<Msg>
pub fn blend(self, bm: BlendMode) -> View<Msg>
Mezcla el nodo entero (su subárbol) contra su backdrop con el modo
bm (CSS mix-blend-mode). El runtime abre una capa aislada
(push_layer(bm, …)) alrededor del rect del nodo que envuelve fill +
contenido + hijos; al cerrarla, el subárbol se compone contra todo lo
pintado antes en el stacking context según bm (p. ej. Mix::Multiply).
Es ortogonal a clip/mask/filter/alpha (un nodo puede llevar todos).
Fase 7.1237.
Limitación v1 (igual que mask/filter): el backdrop es lo que ya está pintado en la escena, no un fondo aislado del subárbol — exacto cuando debajo hay contenido opaco, aproximado si la capa de abajo es el padre.
Sourcepub fn layout_builder<F>(self, builder: F) -> View<Msg>
pub fn layout_builder<F>(self, builder: F) -> View<Msg>
Construye los hijos de este nodo de forma diferida, en función del
tamaño del slot que el layout le asigne (Flutter LayoutBuilder). El
runtime resuelve primero el rect del nodo (una pasada de layout con este
nodo como hoja, sized por su Style/contexto flex) y recién entonces
invoca builder(Constraints) para producir el subárbol — habilitando
paneles responsive cuyo punto de quiebre depende del espacio local,
no de la ventana (para eso alcanza on_resize + el Model).
El Style de este nodo define su tamaño (debe quedar acotado por el
contexto: flex_grow, size definido o percent — no intrínseco a los
hijos, que aún no existen). Cualquier children estático que se haya
seteado se ignora: el builder es la fuente de los hijos.
Límite v1: sin anidamiento — un layout_builder dentro del subárbol
que produce otro layout_builder no se resuelve (queda como hoja).
Sourcepub fn ripple(self, key: u64, color: AlphaColor<Srgb>) -> View<Msg>
pub fn ripple(self, key: u64, color: AlphaColor<Srgb>) -> View<Msg>
Marca este nodo para emitir un ripple/InkWell (la salpicadura de tap
de Material) al recibir un press: un círculo que se expande desde el
punto presionado y se desvanece, recortado al contorno del nodo. key
debe ser estable entre rebuilds del View (índice/hash del item),
igual que la key de Self::animated. color es el tinte de la onda —
usá un color semitransparente (blanco a alpha ~0.25 sobre superficies
oscuras, negro a alpha ~0.12 sobre claras); su alpha se atenúa con el
fade. Es aditivo: convive con on_click/drag sin pisarlos. Duración
por defecto 450 ms; para otra usar Self::ripple_styled.
Sourcepub fn ripple_styled(
self,
key: u64,
color: AlphaColor<Srgb>,
duration: Duration,
) -> View<Msg>
pub fn ripple_styled( self, key: u64, color: AlphaColor<Srgb>, duration: Duration, ) -> View<Msg>
Como Self::ripple pero con la duración explícita de la salpicadura.
Sourcepub fn cursor(self, cursor: Cursor) -> View<Msg>
pub fn cursor(self, cursor: Cursor) -> View<Msg>
Fija la forma del puntero del mouse mientras el cursor está sobre este
nodo (o un descendiente que no declare la suya — se hereda del ancestro
más cercano que la tenga). El runtime la resuelve en el hit-test de hover
y la aplica a la ventana. Ejemplos: .cursor(Cursor::Text) en un input,
.cursor(Cursor::ColResize) en un divisor de splitter,
.cursor(Cursor::Pointer) en un botón.
Sourcepub fn tooltip(self, text: impl Into<String>) -> View<Msg>
pub fn tooltip(self, text: impl Into<String>) -> View<Msg>
Asocia un texto de tooltip a este nodo. Llimphi sólo lo transporta
hasta el MountedNode; el consumidor decide cómo
mostrarlo (un overlay del runtime, una surface popup del cliente) tras
localizar el nodo bajo el cursor con el hit-test de hover.
Sourcepub fn semantics(self, spec: SemanticsSpec) -> View<Msg>
pub fn semantics(self, spec: SemanticsSpec) -> View<Msg>
Declara la semántica accesible completa del nodo de una vez. Usar
cuando ya tenés un SemanticsSpec armado (p. ej. construido por un
widget); para los casos puntuales preferí los atajos
Self::role/Self::aria_label/etc.
Sourcepub fn role(self, role: Role) -> View<Msg>
pub fn role(self, role: Role) -> View<Msg>
Fija el rol semántico del nodo. Si ya había semántica declarada,
preserva label/value/flags y sólo sobreescribe el rol; si no, crea una
SemanticsSpec con sólo el rol.
Sourcepub fn aria_label(self, label: impl Into<Arc<str>>) -> View<Msg>
pub fn aria_label(self, label: impl Into<Arc<str>>) -> View<Msg>
Fija el label accesible (“nombre” que el lector enuncia). Hace falta
cuando el contenido visible del nodo no alcanza (p. ej. un botón con
sólo un ícono). Preserva el resto de la SemanticsSpec si existía.
Sourcepub fn aria_description(self, desc: impl Into<Arc<str>>) -> View<Msg>
pub fn aria_description(self, desc: impl Into<Arc<str>>) -> View<Msg>
Fija la descripción (contexto adicional que el lector enuncia tras el label, típicamente con un atajo). Para info que ayuda pero no es el nombre principal — no abusar (los lectores perciben ruido).
Sourcepub fn aria_value(self, value: impl Into<Arc<str>>) -> View<Msg>
pub fn aria_value(self, value: impl Into<Arc<str>>) -> View<Msg>
Fija el valor (texto del input, valor del slider/spinner). Lo que el lector lee después del label: “Volumen, 70”.
Sourcepub fn aria_checked(self, v: bool) -> View<Msg>
pub fn aria_checked(self, v: bool) -> View<Msg>
Estado checked (checkbox/radio).
Sourcepub fn aria_pressed(self, v: bool) -> View<Msg>
pub fn aria_pressed(self, v: bool) -> View<Msg>
Estado pressed (toggle button).
Sourcepub fn aria_expanded(self, v: bool) -> View<Msg>
pub fn aria_expanded(self, v: bool) -> View<Msg>
Estado expanded (acordeón, menú abierto, tree row expandida).
Sourcepub fn aria_disabled(self, v: bool) -> View<Msg>
pub fn aria_disabled(self, v: bool) -> View<Msg>
Estado disabled — el control no responde a input.
Sourcepub fn aria_readonly(self, v: bool) -> View<Msg>
pub fn aria_readonly(self, v: bool) -> View<Msg>
Estado readonly — el control es visible/seleccionable pero no editable.
Sourcepub fn aria_required(self, v: bool) -> View<Msg>
pub fn aria_required(self, v: bool) -> View<Msg>
Estado required (campo de formulario obligatorio).
Sourcepub fn on_scroll<F>(self, handler: F) -> View<Msg>
pub fn on_scroll<F>(self, handler: F) -> View<Msg>
Registra un handler de rueda local: si el cursor está sobre este
nodo cuando la rueda gira, el runtime lo invoca con el delta
(dx, dy) en líneas lógicas ANTES de caer al App::on_wheel
global. Devolver Some(Msg) consume el evento. Es la base de las
áreas de scroll autocontenidas (llimphi-widget-scroll).
Sourcepub fn on_scale<F>(self, handler: F) -> View<Msg>
pub fn on_scale<F>(self, handler: F) -> View<Msg>
Registra un handler de pinch-to-zoom (gesto de escala). El runtime
lo invoca cuando el cursor está sobre este nodo y el usuario hace un
gesto de escala: Ctrl + rueda en cualquier desktop (camino
universal) o un pinch de trackpad en macOS. El handler recibe
(phase, factor, focal_x, focal_y) — ver ScaleFn: factor es el
cambio multiplicativo incremental (>1 agranda, <1 achica) y
(focal_x, focal_y) es el punto bajo el cursor relativo al rect del
nodo, para zoomear “hacia el cursor”. El típico patrón de canvas:
Msg::Zoom { factor, fx, fy } que multiplica la escala del viewport y
reajusta el pan para mantener el punto focal fijo. Devolver Some(Msg)
consume el gesto (no cae al scroll/on_wheel).
Examples found in repository?
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn on_rotate<F>(self, handler: F) -> View<Msg>
pub fn on_rotate<F>(self, handler: F) -> View<Msg>
Registra un handler de rotación con dos dedos (gesto de trackpad).
El runtime lo invoca cuando el cursor está sobre este nodo y el usuario
rota dos dedos en el trackpad (winit emite RotationGesture sólo en
macOS). El handler recibe (phase, delta_radianes, focal_x, focal_y)
— ver RotateFn: delta_radianes es el incremento angular (positivo
= horario) y (focal_x, focal_y) el punto bajo el cursor relativo al
rect del nodo, para rotar “alrededor del cursor”. Patrón típico de
canvas/imagen: Msg::Rotate { delta, fx, fy } que acumula el ángulo del
viewport. Devolver Some(Msg) consume el gesto.
Sourcepub fn on_double_tap(self, msg: Msg) -> View<Msg>
pub fn on_double_tap(self, msg: Msg) -> View<Msg>
Emite msg en doble-tap (dos clicks izquierdos rápidos y cercanos
sobre este nodo). Aditivo respecto de on_click. Ver
Self::on_double_tap (campo) para la
semántica completa; para la posición del tap usar
Self::on_double_tap_at.
Examples found in repository?
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn on_double_tap_at<F>(self, handler: F) -> View<Msg>
pub fn on_double_tap_at<F>(self, handler: F) -> View<Msg>
Como Self::on_double_tap pero el handler recibe la posición del
segundo tap relativa al rect del nodo (lx, ly, w, h) — para
zoom-to-point o seleccionar la entidad bajo el cursor. Gana sobre
on_double_tap si ambos están.
Sourcepub fn on_long_press(self, msg: Msg) -> View<Msg>
pub fn on_long_press(self, msg: Msg) -> View<Msg>
Emite msg en long-press (mantener el botón ~500 ms sin moverse).
El runtime lo cancela si el cursor se aleja (pasó a drag) o se suelta
antes. Aditivo respecto de on_click/drag. Ver
Self::on_long_press (campo); para la
posición usar Self::on_long_press_at.
Sourcepub fn on_long_press_at<F>(self, handler: F) -> View<Msg>
pub fn on_long_press_at<F>(self, handler: F) -> View<Msg>
Como Self::on_long_press pero el handler recibe la posición del
press relativa al rect del nodo (lx, ly, w, h) — para abrir un menú
contextual en el punto. Gana sobre on_long_press si ambos están.
Examples found in repository?
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn focusable(self, id: u64) -> View<Msg>
pub fn focusable(self, id: u64) -> View<Msg>
Marca este nodo como enfocable con el id opaco id. El runtime lo
incluye en el orden de Tab (pre-orden del árbol) y le da foco al
clickearlo; cada cambio de foco se notifica vía App::on_focus.
El caller pinta el focus-ring comparando el id contra el foco que
guardó en su Model.
Sourcepub fn selectable(self, key: u64) -> View<Msg>
pub fn selectable(self, key: u64) -> View<Msg>
Marca este nodo de texto como seleccionable con el mouse fuera del
editor: arrastrar sobre él resalta el rango y Ctrl/Cmd+C lo copia al
portapapeles. key debe ser estable entre rebuilds del View
(índice, hash del id) — la selección vive en el runtime anclada a esa
key, no al NodeId (que cambia cada frame). Pensá en labels, párrafos,
celdas de tabla, salidas de consola: cualquier texto que el usuario
querría copiar sin un editor. Sólo aplica a texto uniforme (el de
.text(...)/.text_aligned(...)); en nodos con runs/spans no tiene
efecto (esos son del editor / RichText). Componer con el texto:
View::new(style).text_aligned(s, 14.0, col, al).selectable(key).
Examples found in repository?
40 fn view(_: &Self::Model) -> View<Self::Msg> {
41 let mut children: Vec<View<()>> = vec![View::new(Style {
42 size: Size {
43 width: percent(1.0_f32),
44 height: Dimension::auto(),
45 },
46 ..Default::default()
47 })
48 .text_aligned(
49 "Texto seleccionable (arrastrá + Ctrl/Cmd+C)",
50 22.0,
51 Color::from_rgba8(230, 240, 250, 255),
52 Alignment::Start,
53 )];
54
55 for (i, p) in PARRAFOS.iter().enumerate() {
56 children.push(
57 View::new(Style {
58 size: Size {
59 width: percent(1.0_f32),
60 height: Dimension::auto(),
61 },
62 ..Default::default()
63 })
64 .text_aligned(
65 *p,
66 16.0,
67 Color::from_rgba8(205, 214, 226, 255),
68 Alignment::Start,
69 )
70 // La línea clave: cada párrafo es seleccionable con una key
71 // estable (su índice). El resaltado + copy los hace el runtime.
72 .selectable(i as u64),
73 );
74 }
75
76 View::new(Style {
77 flex_direction: FlexDirection::Column,
78 size: Size {
79 width: percent(1.0_f32),
80 height: percent(1.0_f32),
81 },
82 gap: Size {
83 width: length(0.0_f32),
84 height: length(20.0_f32),
85 },
86 padding: Rect {
87 left: length(40.0_f32),
88 right: length(40.0_f32),
89 top: length(36.0_f32),
90 bottom: length(36.0_f32),
91 },
92 align_items: Some(AlignItems::Start),
93 ..Default::default()
94 })
95 .fill(Color::from_rgba8(20, 24, 32, 255))
96 .children(children)
97 }Sourcepub fn hero(self, key: u64, duration: Duration) -> View<Msg>
pub fn hero(self, key: u64, duration: Duration) -> View<Msg>
Marca este nodo como hero shared-element con la key indicada.
Cuando la misma key aparece en un rect distinto en el frame siguiente
(entre rutas, paneles, layouts), el runtime interpola transform para
“volar” del rect anterior al actual durante duration. La key debe
ser estable y única dentro del frame; idéntica semántica a la key
de Self::animated. Para easing distinto al ease-out cúbico default,
ver Self::hero_curve.
Sourcepub fn hero_curve(
self,
key: u64,
duration: Duration,
easing: fn(f32) -> f32,
) -> View<Msg>
pub fn hero_curve( self, key: u64, duration: Duration, easing: fn(f32) -> f32, ) -> View<Msg>
Como Self::hero pero con easing explícito.
Sourcepub fn transform(self, xf: Affine) -> View<Msg>
pub fn transform(self, xf: Affine) -> View<Msg>
Aplica una transformación afín 2D a este nodo y todo su subtree,
alrededor del centro de su rect (CSS transform-origin: 50% 50%). El centro se resuelve en paint contra el layout computado;
el caller sólo provee el afín “local” (producto de sus
rotate/scale/translate). Nodos anidados componen en el
espacio ya transformado del padre. Pensado para transform y
@keyframes CSS de puriy. Affine::IDENTITY equivale a no setear.
Sourcepub fn transform_rel(self, frac: (f64, f64)) -> View<Msg>
pub fn transform_rel(self, frac: (f64, f64)) -> View<Msg>
Traslación relativa al tamaño del propio nodo: (fx, fy) desplaza
(fx · w, fy · h) px, resueltos contra el rect computado en paint.
Es el translate(<%>) de CSS que no cabe en un Affine fijo (p. ej.
el centrado translate(-50%, -50%) ⇒ transform_rel((-0.5, -0.5))).
Compone con transform (si está) como factor más externo. Ver
View::transform. (0.0, 0.0) equivale a no setear.
Sourcepub fn transform_origin(self, pivot: TransformPivot) -> View<Msg>
pub fn transform_origin(self, pivot: TransformPivot) -> View<Msg>
Punto de pivote de transform (CSS transform-origin). Sin setear ⇒
centro del rect (50% 50%). Ver TransformPivot. Sólo tiene efecto
junto con transform/transform_rel.
Sourcepub fn fill(self, color: AlphaColor<Srgb>) -> View<Msg>
pub fn fill(self, color: AlphaColor<Srgb>) -> View<Msg>
Examples found in repository?
70 fn view(model: &Self::Model) -> View<Self::Msg> {
71 let body_text = if model.is_empty() {
72 "tipea algo · ctrl+L limpia · enter salto · backspace borra".to_string()
73 } else {
74 // Cursor visual al final del contenido.
75 format!("{model}\u{2588}")
76 };
77 let body_color = if model.is_empty() {
78 Color::from_rgba8(110, 130, 150, 255)
79 } else {
80 Color::from_rgba8(220, 230, 240, 255)
81 };
82
83 let body = View::new(Style {
84 size: Size {
85 width: percent(1.0_f32),
86 height: percent(1.0_f32),
87 },
88 flex_grow: 1.0,
89 ..Default::default()
90 })
91 .text_aligned(body_text, 22.0, body_color, Alignment::Start);
92
93 let status = View::new(Style {
94 size: Size {
95 width: percent(1.0_f32),
96 height: length(36.0_f32),
97 },
98 ..Default::default()
99 })
100 .fill(Color::from_rgba8(30, 36, 48, 255))
101 .text(
102 format!("{} chars", model.chars().count()),
103 16.0,
104 Color::from_rgba8(160, 180, 200, 255),
105 );
106
107 View::new(Style {
108 flex_direction: FlexDirection::Column,
109 size: Size {
110 width: percent(1.0_f32),
111 height: percent(1.0_f32),
112 },
113 gap: Size {
114 width: length(0.0_f32),
115 height: length(8.0_f32),
116 },
117 padding: llimphi_ui::llimphi_layout::taffy::Rect {
118 left: length(24.0_f32),
119 right: length(24.0_f32),
120 top: length(24.0_f32),
121 bottom: length(24.0_f32),
122 },
123 ..Default::default()
124 })
125 .fill(Color::from_rgba8(20, 24, 32, 255))
126 .children(vec![body, status])
127 }More examples
40 fn view(_: &Self::Model) -> View<Self::Msg> {
41 let mut children: Vec<View<()>> = vec![View::new(Style {
42 size: Size {
43 width: percent(1.0_f32),
44 height: Dimension::auto(),
45 },
46 ..Default::default()
47 })
48 .text_aligned(
49 "Texto seleccionable (arrastrá + Ctrl/Cmd+C)",
50 22.0,
51 Color::from_rgba8(230, 240, 250, 255),
52 Alignment::Start,
53 )];
54
55 for (i, p) in PARRAFOS.iter().enumerate() {
56 children.push(
57 View::new(Style {
58 size: Size {
59 width: percent(1.0_f32),
60 height: Dimension::auto(),
61 },
62 ..Default::default()
63 })
64 .text_aligned(
65 *p,
66 16.0,
67 Color::from_rgba8(205, 214, 226, 255),
68 Alignment::Start,
69 )
70 // La línea clave: cada párrafo es seleccionable con una key
71 // estable (su índice). El resaltado + copy los hace el runtime.
72 .selectable(i as u64),
73 );
74 }
75
76 View::new(Style {
77 flex_direction: FlexDirection::Column,
78 size: Size {
79 width: percent(1.0_f32),
80 height: percent(1.0_f32),
81 },
82 gap: Size {
83 width: length(0.0_f32),
84 height: length(20.0_f32),
85 },
86 padding: Rect {
87 left: length(40.0_f32),
88 right: length(40.0_f32),
89 top: length(36.0_f32),
90 bottom: length(36.0_f32),
91 },
92 align_items: Some(AlignItems::Start),
93 ..Default::default()
94 })
95 .fill(Color::from_rgba8(20, 24, 32, 255))
96 .children(children)
97 }54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }43 fn view(model: &Self::Model) -> View<Self::Msg> {
44 let number = View::new(Style {
45 size: Size {
46 width: percent(1.0_f32),
47 height: Dimension::auto(),
48 },
49 flex_grow: 1.0,
50 align_items: Some(AlignItems::Center),
51 justify_content: Some(JustifyContent::Center),
52 ..Default::default()
53 })
54 .text(model.to_string(), 160.0, Color::from_rgba8(230, 240, 250, 255));
55
56 let increment = View::new(Style {
57 size: Size {
58 width: length(160.0_f32),
59 height: length(56.0_f32),
60 },
61 align_items: Some(AlignItems::Center),
62 justify_content: Some(JustifyContent::Center),
63 ..Default::default()
64 })
65 .fill(Color::from_rgba8(60, 200, 130, 255))
66 .radius(12.0)
67 .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
68 .on_click(Msg::Increment);
69
70 let reset = View::new(Style {
71 size: Size {
72 width: length(120.0_f32),
73 height: length(56.0_f32),
74 },
75 align_items: Some(AlignItems::Center),
76 justify_content: Some(JustifyContent::Center),
77 ..Default::default()
78 })
79 .fill(Color::from_rgba8(220, 80, 80, 255))
80 .radius(12.0)
81 .text("reset", 22.0, Color::from_rgba8(30, 10, 10, 255))
82 .on_click(Msg::Reset);
83
84 let buttons = View::new(Style {
85 flex_direction: FlexDirection::Row,
86 size: Size {
87 width: percent(1.0_f32),
88 height: length(56.0_f32),
89 },
90 gap: Size {
91 width: length(16.0_f32),
92 height: length(0.0_f32),
93 },
94 justify_content: Some(JustifyContent::Center),
95 ..Default::default()
96 })
97 .children(vec![increment, reset]);
98
99 View::new(Style {
100 flex_direction: FlexDirection::Column,
101 size: Size {
102 width: percent(1.0_f32),
103 height: percent(1.0_f32),
104 },
105 gap: Size {
106 width: length(0.0_f32),
107 height: length(24.0_f32),
108 },
109 padding: llimphi_ui::llimphi_layout::taffy::Rect {
110 left: length(32.0_f32),
111 right: length(32.0_f32),
112 top: length(32.0_f32),
113 bottom: length(32.0_f32),
114 },
115 ..Default::default()
116 })
117 .fill(Color::from_rgba8(20, 24, 32, 255))
118 .children(vec![number, buttons])
119 }101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn alpha(self, a: f32) -> View<Msg>
pub fn alpha(self, a: f32) -> View<Msg>
Opacidad uniforme aplicada a este nodo y todos sus descendientes
vía scene.push_layer(Mix::Normal, a, …). Pensado para fade-in/out
de overlays, toasts y modales sin tener que tunear el alpha de
cada color del subtree. Valores fuera de [0.0, 1.0] se clampean.
Hace que el subtree se componga en una capa intermedia — usar sólo
cuando sea necesario (no es gratuito).
Sourcepub fn animated(self, key: u64, duration: Duration) -> View<Msg>
pub fn animated(self, key: u64, duration: Duration) -> View<Msg>
Anima de forma implícita las props de paint de este nodo
(hoy fill y radius): cuando su valor cambia entre frames, el
runtime interpola en duration con ease-out cúbico en vez de saltar
(estilo Flutter AnimatedContainer). key debe ser estable entre
rebuilds del View (índice de item, hash de id) — es lo que enlaza
“el mismo nodo” entre frames; dos nodos distintos no deben compartir
key. La primera aparición no anima; sólo los cambios posteriores. Para
otra curva, Self::animated_curve.
Sourcepub fn animated_size(self, key: u64, duration: Duration) -> View<Msg>
pub fn animated_size(self, key: u64, duration: Duration) -> View<Msg>
Anima de forma implícita el tamaño de este nodo (Flutter
AnimatedSize / Compose animateContentSize()). Cuando
style.size cambia entre frames, el runtime interpola en
duration con ease-out cúbico en vez de saltar — siblings y
hijos reflowean suave porque el reconciler parcha style.size
antes del layout. key debe ser estable entre rebuilds.
Para otra curva, Self::animated_size_curve. Bloque 15.
Límite v1: ambos style.size.width y style.size.height
tienen que ser Dimension::Length(_). Si una es Percent/Auto,
el nodo se monta tal cual sin animación (no hay valor en píxeles
estable para interpolar). El caller que necesite animar un nodo
flex puede envolver el contenido en un wrap con length(...)
fijo y mover el flex al padre.
Sourcepub fn animated_size_curve(
self,
key: u64,
duration: Duration,
easing: fn(f32) -> f32,
) -> View<Msg>
pub fn animated_size_curve( self, key: u64, duration: Duration, easing: fn(f32) -> f32, ) -> View<Msg>
Como Self::animated_size pero con curva de easing custom.
Sourcepub fn animated_enter(self, key: u64, duration: Duration) -> View<Msg>
pub fn animated_enter(self, key: u64, duration: Duration) -> View<Msg>
Como Self::animated pero además anima la entrada: la primera vez
que esta key aparece, su opacidad sube de 0 a su valor (alpha o 1.0)
en duration — fade-in estilo AnimatedSwitcher/AnimatedVisibility.
Útil para toasts, items de lista que aparecen, paneles que se montan,
resultados que entran. Como toda animación implícita, depende de una
key estable; reutilizar la key de un nodo que ya estaba NO refadea
(sólo la primera aparición anima). Para animar también la salida, ver
Self::animated_inout.
Sourcepub fn animated_enter_from(
self,
key: u64,
duration: Duration,
from_xf: Affine,
) -> View<Msg>
pub fn animated_enter_from( self, key: u64, duration: Duration, from_xf: Affine, ) -> View<Msg>
Como Self::animated_enter pero además arranca la entrada desde una
transformación afín específica hacia la del nodo (o la identidad si
no setea .transform). Habilita scale-in / slide-in / rotate-in
implícitos: el caller declara la pose inicial, el runtime interpola.
Ejemplos:
Affine::scale(0.6)→ “pop” (FAB de Material, modales).Affine::translate((0.0, 60.0))→ slide-in vertical (snackbars).Affine::translate((-w, 0.0))→ slide-in lateral (drawers). Combina con el fade-in de entrada (alpha 0 → opaque). Sin animación de salida; para entrada+salida con pose, verSelf::animated_inout_from.
Sourcepub fn animated_pop_in(self, key: u64, duration: Duration) -> View<Msg>
pub fn animated_pop_in(self, key: u64, duration: Duration) -> View<Msg>
Atajo Material: scale-in desde 0.6 (entrada “pop” del FAB). Combina con
fade-in. Equivalente a .animated_enter_from(key, dur, Affine::scale(0.6)).
Sourcepub fn animated_exit(self, key: u64, duration: Duration) -> View<Msg>
pub fn animated_exit(self, key: u64, duration: Duration) -> View<Msg>
Anima la salida (fade-out): cuando esta key desaparece del árbol,
el runtime retiene la última subescena que pintó y la reproduce con
opacidad decreciente durante duration — estilo AnimatedSwitcher /
AnimatedVisibility al ocultarse. No anima la entrada (para ambas, ver
Self::animated_inout). Tiene coste por frame mientras el nodo vive
(captura su subárbol); usar con moderación (toasts, modales, paneles).
Sourcepub fn animated_inout(self, key: u64, duration: Duration) -> View<Msg>
pub fn animated_inout(self, key: u64, duration: Duration) -> View<Msg>
Anima entrada y salida: fade-in en la primera aparición y fade-out al
desmontarse, ambos en duration. La pieza completa de “animación de
contenido” para un nodo que aparece y desaparece (un toast, un panel que
se abre y cierra, un resultado que entra y se va).
Sourcepub fn animated_inout_from(
self,
key: u64,
duration: Duration,
from_xf: Affine,
) -> View<Msg>
pub fn animated_inout_from( self, key: u64, duration: Duration, from_xf: Affine, ) -> View<Msg>
Como Self::animated_inout pero arranca la entrada desde la
transformación afín from_xf (igual semántica que
Self::animated_enter_from). La salida sigue siendo el fade-out
estándar (la subescena retenida no transforma).
Sourcepub fn animated_curve(
self,
key: u64,
duration: Duration,
easing: fn(f32) -> f32,
) -> View<Msg>
pub fn animated_curve( self, key: u64, duration: Duration, easing: fn(f32) -> f32, ) -> View<Msg>
Como Self::animated pero con easing explícito (p. ej.
llimphi_theme::motion::ease_in_out_cubic).
Sourcepub fn animated_switch(
self,
key: u64,
variant: u64,
duration: Duration,
) -> View<Msg>
pub fn animated_switch( self, key: u64, variant: u64, duration: Duration, ) -> View<Msg>
Cross-fade real entre variantes de contenido bajo la misma key
(Flutter AnimatedSwitcher). variant identifica el contenido actual
(índice de pestaña, hash del estado, discriminante de un enum…). Cuando
variant cambia entre frames, el runtime desvanece la subescena vieja
(fade-out, retenida del frame previo) mientras hace fade-in del subárbol
nuevo, en el mismo rect — la transición real entre dos identidades, en
vez de combinar animated_enter+animated_exit de dos keys distintas.
Envolvé el contenido conmutable en un nodo con esta marca; sus hijos son
el contenido. La primera aparición no cruza (sólo fija la variante).
Igual que exit, captura el subárbol por frame — usar en pocos nodos
(un panel central, un visor que cambia de documento), no por fila.
Sourcepub fn hover_fill(self, color: AlphaColor<Srgb>) -> View<Msg>
pub fn hover_fill(self, color: AlphaColor<Srgb>) -> View<Msg>
Color a usar cuando el cursor está sobre este nodo. Habilita el hit-test de hover sobre el nodo.
Sourcepub fn draggable<F>(self, handler: F) -> View<Msg>
pub fn draggable<F>(self, handler: F) -> View<Msg>
Marca este nodo como draggable. Mientras el usuario sostenga el
botón izquierdo sobre él, el runtime llama handler(Move, dx, dy)
por cada CursorMoved (dx/dy = delta desde el evento anterior) y
handler(End, 0, 0) al soltar. Sobreescribe on_click para este
nodo: un nodo es draggable o clickable.
Examples found in repository?
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn draggable_at<F>(self, handler: F) -> View<Msg>
pub fn draggable_at<F>(self, handler: F) -> View<Msg>
Como draggable, pero el handler también recibe la posición
inicial del press relativa al rect del nodo (initial_lx, initial_ly). Útil cuando el caller necesita resolver qué
entidad bajo el cursor inició el drag (Conceptos, lemmings,
nodos de un grafo, etc.). Gana sobre draggable si ambos están.
Sourcepub fn draggable_velocity<F>(self, handler: F) -> View<Msg>
pub fn draggable_velocity<F>(self, handler: F) -> View<Msg>
Como Self::draggable pero el handler recibe además la velocidad
del drag al soltarlo (vx, vy en px/s) en la fase
DragPhase::End — Fn(DragPhase, dx, dy, vx, vy) -> Option<Msg>. El
runtime mide el desplazamiento sobre los últimos ~100 ms y lo divide
por el tiempo transcurrido. Durante DragPhase::Move, vx == vy == 0
(la velocidad sólo se calcula al final). Gana sobre draggable y
draggable_at si conviven en el mismo nodo — un nodo elige un
único sabor de drag. Habilita fling-desde-drag: el caller emite
Msg::Fling { vx, vy } en End y arranca un ticker que decae la
velocidad con [fling_step] hasta asentar.
Sourcepub fn drag_payload(self, payload: u64) -> View<Msg>
pub fn drag_payload(self, payload: u64) -> View<Msg>
Declara el payload u64 que viaja con el drag de este nodo. Los
drop targets bajo cursor al soltar reciben este valor en su
on_drop. Sin payload, los drop targets no reaccionan (útil para
drags de “resize/scroll” que no representan transferencia).
Sourcepub fn on_drop<F>(self, handler: F) -> View<Msg>
pub fn on_drop<F>(self, handler: F) -> View<Msg>
Marca este nodo como drop target. El runtime invoca handler(payload)
cuando un drag termina sobre el rect de este nodo y el origen del
drag declaró un payload. Si devuelve Some(Msg), se dispatchea al
update antes del DragPhase::End del origen.
Sourcepub fn drop_hover_fill(self, color: AlphaColor<Srgb>) -> View<Msg>
pub fn drop_hover_fill(self, color: AlphaColor<Srgb>) -> View<Msg>
Color de relleno cuando un drag activo está hovereando este drop
target. Análogo a hover_fill pero solo aplica mientras dura un
drag. Útil para resaltar el destino válido.
Sourcepub fn radius(self, r: f64) -> View<Msg>
pub fn radius(self, r: f64) -> View<Msg>
Examples found in repository?
54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }More examples
43 fn view(model: &Self::Model) -> View<Self::Msg> {
44 let number = View::new(Style {
45 size: Size {
46 width: percent(1.0_f32),
47 height: Dimension::auto(),
48 },
49 flex_grow: 1.0,
50 align_items: Some(AlignItems::Center),
51 justify_content: Some(JustifyContent::Center),
52 ..Default::default()
53 })
54 .text(model.to_string(), 160.0, Color::from_rgba8(230, 240, 250, 255));
55
56 let increment = View::new(Style {
57 size: Size {
58 width: length(160.0_f32),
59 height: length(56.0_f32),
60 },
61 align_items: Some(AlignItems::Center),
62 justify_content: Some(JustifyContent::Center),
63 ..Default::default()
64 })
65 .fill(Color::from_rgba8(60, 200, 130, 255))
66 .radius(12.0)
67 .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
68 .on_click(Msg::Increment);
69
70 let reset = View::new(Style {
71 size: Size {
72 width: length(120.0_f32),
73 height: length(56.0_f32),
74 },
75 align_items: Some(AlignItems::Center),
76 justify_content: Some(JustifyContent::Center),
77 ..Default::default()
78 })
79 .fill(Color::from_rgba8(220, 80, 80, 255))
80 .radius(12.0)
81 .text("reset", 22.0, Color::from_rgba8(30, 10, 10, 255))
82 .on_click(Msg::Reset);
83
84 let buttons = View::new(Style {
85 flex_direction: FlexDirection::Row,
86 size: Size {
87 width: percent(1.0_f32),
88 height: length(56.0_f32),
89 },
90 gap: Size {
91 width: length(16.0_f32),
92 height: length(0.0_f32),
93 },
94 justify_content: Some(JustifyContent::Center),
95 ..Default::default()
96 })
97 .children(vec![increment, reset]);
98
99 View::new(Style {
100 flex_direction: FlexDirection::Column,
101 size: Size {
102 width: percent(1.0_f32),
103 height: percent(1.0_f32),
104 },
105 gap: Size {
106 width: length(0.0_f32),
107 height: length(24.0_f32),
108 },
109 padding: llimphi_ui::llimphi_layout::taffy::Rect {
110 left: length(32.0_f32),
111 right: length(32.0_f32),
112 top: length(32.0_f32),
113 bottom: length(32.0_f32),
114 },
115 ..Default::default()
116 })
117 .fill(Color::from_rgba8(20, 24, 32, 255))
118 .children(vec![number, buttons])
119 }Sourcepub fn radius_corners(self, tl: f64, tr: f64, br: f64, bl: f64) -> View<Msg>
pub fn radius_corners(self, tl: f64, tr: f64, br: f64, bl: f64) -> View<Msg>
Radio por esquina (top-left, top-right, bottom-right, bottom-left,
en sentido horario desde arriba-izquierda) — CSS border-radius con
cuatro valores. Sobreescribe a Self::radius mientras esté presente.
Para cards con sólo las esquinas de arriba redondeadas, pestañas,
bocadillos de chat asimétricos, etc. El borde respeta las cuatro
esquinas; la sombra sigue usando el radius escalar (el blur
nativo de vello no acepta radios por esquina).
Sourcepub fn shadow(self, shadow: Shadow) -> View<Msg>
pub fn shadow(self, shadow: Shadow) -> View<Msg>
Proyecta una sombra detrás del nodo (drop shadow), rasterizada con
el blur gaussiano nativo de vello. Se pinta antes del relleno, así
el fill opaco la tapa y la sombra asoma por el desenfoque/offset.
El radio de la sombra sigue al del nodo (más el spread). Ver
Shadow (Shadow::soft(alpha, blur) es el default tasteful).
Sourcepub fn fill_gradient(self, gradient: Gradient) -> View<Msg>
pub fn fill_gradient(self, gradient: Gradient) -> View<Msg>
Rellena el nodo con un gradiente en vez de un color sólido. El
gradiente se autorea en el cuadrado unidad [0,1]² y el runtime
lo mapea al rect del nodo (así no necesitás saber el tamaño al
construir el View) — igual que Alignment relativo de Flutter.
use llimphi_ui::llimphi_raster::peniko::{Color, Gradient};
use llimphi_ui::llimphi_raster::kurbo::Point;
// vertical: arriba claro → abajo oscuro
let g = Gradient::new_linear(Point::new(0.0, 0.0), Point::new(0.0, 1.0))
.with_stops([Color::from_rgba8(80,90,110,255), Color::from_rgba8(30,34,44,255)].as_slice());
view.fill_gradient(g)Gana sobre fill como base; un hover_fill (color) lo sigue
overrideando mientras el cursor está encima.
Sourcepub fn border(self, width: f64, color: AlphaColor<Srgb>) -> View<Msg>
pub fn border(self, width: f64, color: AlphaColor<Srgb>) -> View<Msg>
Dibuja un borde (stroke) sobre el contorno redondeado del nodo, inset media línea hacia adentro (el grosor queda dentro del rect). Reemplaza el viejo truco de envolver el nodo en un rect-padre del color del borde con padding de 1px.
Sourcepub fn text(
self,
content: impl Into<String>,
size_px: f32,
color: AlphaColor<Srgb>,
) -> View<Msg>
pub fn text( self, content: impl Into<String>, size_px: f32, color: AlphaColor<Srgb>, ) -> View<Msg>
Examples found in repository?
70 fn view(model: &Self::Model) -> View<Self::Msg> {
71 let body_text = if model.is_empty() {
72 "tipea algo · ctrl+L limpia · enter salto · backspace borra".to_string()
73 } else {
74 // Cursor visual al final del contenido.
75 format!("{model}\u{2588}")
76 };
77 let body_color = if model.is_empty() {
78 Color::from_rgba8(110, 130, 150, 255)
79 } else {
80 Color::from_rgba8(220, 230, 240, 255)
81 };
82
83 let body = View::new(Style {
84 size: Size {
85 width: percent(1.0_f32),
86 height: percent(1.0_f32),
87 },
88 flex_grow: 1.0,
89 ..Default::default()
90 })
91 .text_aligned(body_text, 22.0, body_color, Alignment::Start);
92
93 let status = View::new(Style {
94 size: Size {
95 width: percent(1.0_f32),
96 height: length(36.0_f32),
97 },
98 ..Default::default()
99 })
100 .fill(Color::from_rgba8(30, 36, 48, 255))
101 .text(
102 format!("{} chars", model.chars().count()),
103 16.0,
104 Color::from_rgba8(160, 180, 200, 255),
105 );
106
107 View::new(Style {
108 flex_direction: FlexDirection::Column,
109 size: Size {
110 width: percent(1.0_f32),
111 height: percent(1.0_f32),
112 },
113 gap: Size {
114 width: length(0.0_f32),
115 height: length(8.0_f32),
116 },
117 padding: llimphi_ui::llimphi_layout::taffy::Rect {
118 left: length(24.0_f32),
119 right: length(24.0_f32),
120 top: length(24.0_f32),
121 bottom: length(24.0_f32),
122 },
123 ..Default::default()
124 })
125 .fill(Color::from_rgba8(20, 24, 32, 255))
126 .children(vec![body, status])
127 }More examples
54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }43 fn view(model: &Self::Model) -> View<Self::Msg> {
44 let number = View::new(Style {
45 size: Size {
46 width: percent(1.0_f32),
47 height: Dimension::auto(),
48 },
49 flex_grow: 1.0,
50 align_items: Some(AlignItems::Center),
51 justify_content: Some(JustifyContent::Center),
52 ..Default::default()
53 })
54 .text(model.to_string(), 160.0, Color::from_rgba8(230, 240, 250, 255));
55
56 let increment = View::new(Style {
57 size: Size {
58 width: length(160.0_f32),
59 height: length(56.0_f32),
60 },
61 align_items: Some(AlignItems::Center),
62 justify_content: Some(JustifyContent::Center),
63 ..Default::default()
64 })
65 .fill(Color::from_rgba8(60, 200, 130, 255))
66 .radius(12.0)
67 .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
68 .on_click(Msg::Increment);
69
70 let reset = View::new(Style {
71 size: Size {
72 width: length(120.0_f32),
73 height: length(56.0_f32),
74 },
75 align_items: Some(AlignItems::Center),
76 justify_content: Some(JustifyContent::Center),
77 ..Default::default()
78 })
79 .fill(Color::from_rgba8(220, 80, 80, 255))
80 .radius(12.0)
81 .text("reset", 22.0, Color::from_rgba8(30, 10, 10, 255))
82 .on_click(Msg::Reset);
83
84 let buttons = View::new(Style {
85 flex_direction: FlexDirection::Row,
86 size: Size {
87 width: percent(1.0_f32),
88 height: length(56.0_f32),
89 },
90 gap: Size {
91 width: length(16.0_f32),
92 height: length(0.0_f32),
93 },
94 justify_content: Some(JustifyContent::Center),
95 ..Default::default()
96 })
97 .children(vec![increment, reset]);
98
99 View::new(Style {
100 flex_direction: FlexDirection::Column,
101 size: Size {
102 width: percent(1.0_f32),
103 height: percent(1.0_f32),
104 },
105 gap: Size {
106 width: length(0.0_f32),
107 height: length(24.0_f32),
108 },
109 padding: llimphi_ui::llimphi_layout::taffy::Rect {
110 left: length(32.0_f32),
111 right: length(32.0_f32),
112 top: length(32.0_f32),
113 bottom: length(32.0_f32),
114 },
115 ..Default::default()
116 })
117 .fill(Color::from_rgba8(20, 24, 32, 255))
118 .children(vec![number, buttons])
119 }101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn text_aligned(
self,
content: impl Into<String>,
size_px: f32,
color: AlphaColor<Srgb>,
alignment: Alignment,
) -> View<Msg>
pub fn text_aligned( self, content: impl Into<String>, size_px: f32, color: AlphaColor<Srgb>, alignment: Alignment, ) -> View<Msg>
Examples found in repository?
70 fn view(model: &Self::Model) -> View<Self::Msg> {
71 let body_text = if model.is_empty() {
72 "tipea algo · ctrl+L limpia · enter salto · backspace borra".to_string()
73 } else {
74 // Cursor visual al final del contenido.
75 format!("{model}\u{2588}")
76 };
77 let body_color = if model.is_empty() {
78 Color::from_rgba8(110, 130, 150, 255)
79 } else {
80 Color::from_rgba8(220, 230, 240, 255)
81 };
82
83 let body = View::new(Style {
84 size: Size {
85 width: percent(1.0_f32),
86 height: percent(1.0_f32),
87 },
88 flex_grow: 1.0,
89 ..Default::default()
90 })
91 .text_aligned(body_text, 22.0, body_color, Alignment::Start);
92
93 let status = View::new(Style {
94 size: Size {
95 width: percent(1.0_f32),
96 height: length(36.0_f32),
97 },
98 ..Default::default()
99 })
100 .fill(Color::from_rgba8(30, 36, 48, 255))
101 .text(
102 format!("{} chars", model.chars().count()),
103 16.0,
104 Color::from_rgba8(160, 180, 200, 255),
105 );
106
107 View::new(Style {
108 flex_direction: FlexDirection::Column,
109 size: Size {
110 width: percent(1.0_f32),
111 height: percent(1.0_f32),
112 },
113 gap: Size {
114 width: length(0.0_f32),
115 height: length(8.0_f32),
116 },
117 padding: llimphi_ui::llimphi_layout::taffy::Rect {
118 left: length(24.0_f32),
119 right: length(24.0_f32),
120 top: length(24.0_f32),
121 bottom: length(24.0_f32),
122 },
123 ..Default::default()
124 })
125 .fill(Color::from_rgba8(20, 24, 32, 255))
126 .children(vec![body, status])
127 }More examples
40 fn view(_: &Self::Model) -> View<Self::Msg> {
41 let mut children: Vec<View<()>> = vec![View::new(Style {
42 size: Size {
43 width: percent(1.0_f32),
44 height: Dimension::auto(),
45 },
46 ..Default::default()
47 })
48 .text_aligned(
49 "Texto seleccionable (arrastrá + Ctrl/Cmd+C)",
50 22.0,
51 Color::from_rgba8(230, 240, 250, 255),
52 Alignment::Start,
53 )];
54
55 for (i, p) in PARRAFOS.iter().enumerate() {
56 children.push(
57 View::new(Style {
58 size: Size {
59 width: percent(1.0_f32),
60 height: Dimension::auto(),
61 },
62 ..Default::default()
63 })
64 .text_aligned(
65 *p,
66 16.0,
67 Color::from_rgba8(205, 214, 226, 255),
68 Alignment::Start,
69 )
70 // La línea clave: cada párrafo es seleccionable con una key
71 // estable (su índice). El resaltado + copy los hace el runtime.
72 .selectable(i as u64),
73 );
74 }
75
76 View::new(Style {
77 flex_direction: FlexDirection::Column,
78 size: Size {
79 width: percent(1.0_f32),
80 height: percent(1.0_f32),
81 },
82 gap: Size {
83 width: length(0.0_f32),
84 height: length(20.0_f32),
85 },
86 padding: Rect {
87 left: length(40.0_f32),
88 right: length(40.0_f32),
89 top: length(36.0_f32),
90 bottom: length(36.0_f32),
91 },
92 align_items: Some(AlignItems::Start),
93 ..Default::default()
94 })
95 .fill(Color::from_rgba8(20, 24, 32, 255))
96 .children(children)
97 }Sourcepub fn text_aligned_italic(
self,
content: impl Into<String>,
size_px: f32,
color: AlphaColor<Srgb>,
alignment: Alignment,
italic: bool,
) -> View<Msg>
pub fn text_aligned_italic( self, content: impl Into<String>, size_px: f32, color: AlphaColor<Srgb>, alignment: Alignment, italic: bool, ) -> View<Msg>
Como text_aligned pero con un flag italic. Si la fuente activa
no tiene variante italic, parley aplica synthesizing.
Sourcepub fn text_aligned_full(
self,
content: impl Into<String>,
size_px: f32,
color: AlphaColor<Srgb>,
alignment: Alignment,
italic: bool,
font_family: Option<String>,
) -> View<Msg>
pub fn text_aligned_full( self, content: impl Into<String>, size_px: f32, color: AlphaColor<Srgb>, alignment: Alignment, italic: bool, font_family: Option<String>, ) -> View<Msg>
Como text_aligned_italic pero con font-family explícito.
La cadena se pasa como parley::FontStack::Source (acepta listas
CSS con fallbacks).
Sourcepub fn text_runs(
self,
content: impl Into<String>,
size_px: f32,
default_color: AlphaColor<Srgb>,
runs: Vec<(usize, usize, AlphaColor<Srgb>)>,
alignment: Alignment,
) -> View<Msg>
pub fn text_runs( self, content: impl Into<String>, size_px: f32, default_color: AlphaColor<Srgb>, runs: Vec<(usize, usize, AlphaColor<Srgb>)>, alignment: Alignment, ) -> View<Msg>
Texto multicolor en una sola pasada de shaping: content se pinta
con default_color y cada (start_byte, end_byte, color) de runs
sobreescribe su rango (offsets en bytes). Pensado para syntax
highlighting — un nodo por línea en vez de uno por token. Anclado
arriba-izquierda (sin centrado vertical); el caller dimensiona el rect.
Sourcepub fn text_spans(
self,
content: impl Into<String>,
size_px: f32,
default_color: AlphaColor<Srgb>,
spans: Vec<TextSpan>,
alignment: Alignment,
) -> View<Msg>
pub fn text_spans( self, content: impl Into<String>, size_px: f32, default_color: AlphaColor<Srgb>, spans: Vec<TextSpan>, alignment: Alignment, ) -> View<Msg>
Texto RichText (Bloque 13 de PARIDAD-FLUTTER, cierra Tier 2):
content se pinta con los defaults del bloque (size_px,
default_color, alignment, weight 400, no italic, line-height 1.2,
fuente default) y cada llimphi_text::TextSpan sobreescribe en su
rango de bytes uno o más de
size_px/weight/italic/font_family/color/underline/
strikethrough. Soporta wrap (el ancho lo fija el layout taffy del
nodo); apto para párrafos con un <b>/<i>/<code>/<small>
inline, links subrayados, headings dentro del mismo flujo, render
barato de markdown.
Sourcepub fn with_spans(self, spans: Vec<TextSpan>) -> View<Msg>
pub fn with_spans(self, spans: Vec<TextSpan>) -> View<Msg>
Adjunta o reemplaza los TextSpec::spans del texto ya seteado
(RichText). Permite construir el texto con los builders uniformes
(.text_aligned(...).bold().underline()) y luego inyectar overrides
inline. No-op si el nodo no tiene texto.
Sourcepub fn line_height(self, mult: f32) -> View<Msg>
pub fn line_height(self, mult: f32) -> View<Msg>
Sobreescribe el múltiplo de interlínea del texto ya seteado (default
1.2). No-op si el nodo no tiene texto. Pensado para puriy, que pasa
el line-height computado de CSS para que medición y pintado usen
el mismo valor.
Sourcepub fn text_weight(self, weight: f32) -> View<Msg>
pub fn text_weight(self, weight: f32) -> View<Msg>
Sobreescribe el peso de fuente del texto ya seteado (default 400 = normal). Convención CSS: 400 normal, 500 medium, 600 semibold, 700 bold. parley elige la variante más cercana de la familia activa o la sintetiza. No-op si el nodo no tiene texto. Afecta medida y pintado.
Sourcepub fn bold(self) -> View<Msg>
pub fn bold(self) -> View<Msg>
Atajo de Self::text_weight a 700 (bold). No-op sin texto.
Sourcepub fn mono(self) -> View<Msg>
pub fn mono(self) -> View<Msg>
Fija la familia de fuente del texto ya seteado a la monoespaciada
embebida (llimphi_text::MONOSPACE) — ancho fijo garantizado para
que ls, tablas y logs columneen. No-op si el nodo no tiene texto.
Afecta medida y pintado.
Sourcepub fn max_lines(self, n: usize) -> View<Msg>
pub fn max_lines(self, n: usize) -> View<Msg>
Clampa el texto a n líneas sin glifo de ellipsis (corte seco del
prefijo que cupo). CSS -webkit-line-clamp sin text-overflow. No-op
sin texto. Para el corte con … usar Self::ellipsis. Sólo trunca si
hay envoltura (requiere ancho acotado por el layout).
Sourcepub fn ellipsis(self, n: usize) -> View<Msg>
pub fn ellipsis(self, n: usize) -> View<Msg>
Clampa el texto a n líneas terminando la última en … cuando excede
(CSS text-overflow: ellipsis + -webkit-line-clamp: n). Lo más común
para items de lista, celdas de tabla, breadcrumbs y labels en cajas
dimensionadas. n = 1 es el clásico single-line ellipsis. No-op sin
texto.
Sourcepub fn letter_spacing(self, px: f32) -> View<Msg>
pub fn letter_spacing(self, px: f32) -> View<Msg>
letter-spacing (CSS): px extra entre letras (0 = normal, negativo
junta). Afecta medida y pintado. No-op sin texto. Sólo el camino
uniforme; el RichText con spans lo ignora en v1.
Sourcepub fn word_spacing(self, px: f32) -> View<Msg>
pub fn word_spacing(self, px: f32) -> View<Msg>
word-spacing (CSS): px extra entre palabras (0 = normal). Mismo
régimen que Self::letter_spacing. No-op sin texto.
Sourcepub fn no_wrap(self) -> View<Msg>
pub fn no_wrap(self) -> View<Msg>
white-space: nowrap/pre (CSS): el texto no envuelve — se shapea
en una sola línea (break_all_lines(None)) sin importar el ancho de la
caja, y desborda (lo recorta overflow: hidden si lo hay). Afecta
medida y pintado. No-op sin texto. Sólo el camino uniforme; el RichText
con spans lo ignora en v1.
Sourcepub fn overflow_wrap(self) -> View<Msg>
pub fn overflow_wrap(self) -> View<Msg>
overflow-wrap: break-word/anywhere (CSS): una palabra más ancha que
la caja se parte para que entre, en vez de desbordar. Afecta medida
y pintado. No-op sin texto. Sólo el camino uniforme; el RichText con
spans lo ignora en v1, igual que no_wrap.
Sourcepub fn underline(self) -> View<Msg>
pub fn underline(self) -> View<Msg>
Activa subrayado del texto (CSS text-decoration: underline / Flutter
TextDecoration.underline). parley registra la decoración por run y el
runtime pinta la línea bajo la base usando underline_offset y
underline_size del font metric — proporcional al tamaño de fuente
elegido. No-op sin texto.
Sourcepub fn strikethrough(self) -> View<Msg>
pub fn strikethrough(self) -> View<Msg>
Activa tachado del texto (CSS text-decoration: line-through /
Flutter TextDecoration.lineThrough). Mismo régimen que Self::underline
pero usando el strikethrough metric. No-op sin texto.
Sourcepub fn on_click(self, msg: Msg) -> View<Msg>
pub fn on_click(self, msg: Msg) -> View<Msg>
Examples found in repository?
54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }More examples
43 fn view(model: &Self::Model) -> View<Self::Msg> {
44 let number = View::new(Style {
45 size: Size {
46 width: percent(1.0_f32),
47 height: Dimension::auto(),
48 },
49 flex_grow: 1.0,
50 align_items: Some(AlignItems::Center),
51 justify_content: Some(JustifyContent::Center),
52 ..Default::default()
53 })
54 .text(model.to_string(), 160.0, Color::from_rgba8(230, 240, 250, 255));
55
56 let increment = View::new(Style {
57 size: Size {
58 width: length(160.0_f32),
59 height: length(56.0_f32),
60 },
61 align_items: Some(AlignItems::Center),
62 justify_content: Some(JustifyContent::Center),
63 ..Default::default()
64 })
65 .fill(Color::from_rgba8(60, 200, 130, 255))
66 .radius(12.0)
67 .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
68 .on_click(Msg::Increment);
69
70 let reset = View::new(Style {
71 size: Size {
72 width: length(120.0_f32),
73 height: length(56.0_f32),
74 },
75 align_items: Some(AlignItems::Center),
76 justify_content: Some(JustifyContent::Center),
77 ..Default::default()
78 })
79 .fill(Color::from_rgba8(220, 80, 80, 255))
80 .radius(12.0)
81 .text("reset", 22.0, Color::from_rgba8(30, 10, 10, 255))
82 .on_click(Msg::Reset);
83
84 let buttons = View::new(Style {
85 flex_direction: FlexDirection::Row,
86 size: Size {
87 width: percent(1.0_f32),
88 height: length(56.0_f32),
89 },
90 gap: Size {
91 width: length(16.0_f32),
92 height: length(0.0_f32),
93 },
94 justify_content: Some(JustifyContent::Center),
95 ..Default::default()
96 })
97 .children(vec![increment, reset]);
98
99 View::new(Style {
100 flex_direction: FlexDirection::Column,
101 size: Size {
102 width: percent(1.0_f32),
103 height: percent(1.0_f32),
104 },
105 gap: Size {
106 width: length(0.0_f32),
107 height: length(24.0_f32),
108 },
109 padding: llimphi_ui::llimphi_layout::taffy::Rect {
110 left: length(32.0_f32),
111 right: length(32.0_f32),
112 top: length(32.0_f32),
113 bottom: length(32.0_f32),
114 },
115 ..Default::default()
116 })
117 .fill(Color::from_rgba8(20, 24, 32, 255))
118 .children(vec![number, buttons])
119 }Sourcepub fn on_pointer_enter(self, msg: Msg) -> View<Msg>
pub fn on_pointer_enter(self, msg: Msg) -> View<Msg>
Dispatch msg cuando el cursor entra al rect del nodo
(transición no-hover → hover). Sólo emite una vez por entrada —
el runtime no repite el msg si el cursor se mueve dentro del rect.
Sourcepub fn on_pointer_leave(self, msg: Msg) -> View<Msg>
pub fn on_pointer_leave(self, msg: Msg) -> View<Msg>
Dispatch msg cuando el cursor sale del rect del nodo.
Sourcepub fn on_pointer_move_at<F>(self, handler: F) -> View<Msg>
pub fn on_pointer_move_at<F>(self, handler: F) -> View<Msg>
Handler de movimiento del cursor sobre el nodo. Recibe (local_x, local_y, rect_w, rect_h) (posición relativa al rect del nodo) en CADA
CursorMoved mientras el cursor está encima — no sólo al entrar, a
diferencia de Self::on_pointer_enter. Útil para seguir el cursor:
thumbnail de hover sobre un timeline, drawer que reacciona a la posición.
Devolver None no dispara update.
Sourcepub fn on_click_at<F>(self, handler: F) -> View<Msg>
pub fn on_click_at<F>(self, handler: F) -> View<Msg>
Como on_click, pero el handler recibe (local_x, local_y, rect_w, rect_h) — la posición del cursor relativa al rect del
nodo más las dimensiones actuales del nodo. Útil para canvas
elements que necesitan saber dónde fue el click para convertirlo
a coordenadas de mundo. Sobrescribe on_click para este nodo
si ambos están presentes.
Sourcepub fn on_right_click(self, msg: Msg) -> View<Msg>
pub fn on_right_click(self, msg: Msg) -> View<Msg>
Declara el Msg a emitir cuando el usuario hace click derecho
sobre este nodo. Para menús contextuales, conviene pasar un
Msg::OpenMenu { ... } y dejar que el modelo guarde la
posición; el overlay se abre vía [App::view_overlay].
Sourcepub fn on_right_click_at<F>(self, handler: F) -> View<Msg>
pub fn on_right_click_at<F>(self, handler: F) -> View<Msg>
Variante posicional de Self::on_right_click. El handler recibe
(local_x, local_y, rect_w, rect_h) para que un nodo “grilla”
pueda resolver internamente qué subcelda recibió el click. La
posición está relativa al rect del nodo.
Sourcepub fn on_middle_click(self, msg: Msg) -> View<Msg>
pub fn on_middle_click(self, msg: Msg) -> View<Msg>
Declara el Msg a emitir cuando el usuario hace click con el
botón del medio (rueda presionada). Usado típicamente para abrir
links en pestaña nueva — igual que Ctrl+Click pero más rápido.
Sourcepub fn image(self, image: ImageBrush) -> View<Msg>
pub fn image(self, image: ImageBrush) -> View<Msg>
Pinta image dentro del rect del nodo. El encaje default es
ImageFit::Contain (preservar aspect ratio cabiendo);
usar Self::image_fit para Cover/Fill/None. El clip
respeta radius/corner_radii, así avatares y cards
redondeadas funcionan sin envolver en clip(true). Re-exporta
peniko::Image vía llimphi_raster::peniko::Image — el
caller decodifica los bytes con el crate image (u otro) y
construye el Image con Blob<u8> + ImageFormat::Rgba8.
Sourcepub fn image_fit(self, fit: ImageFit) -> View<Msg>
pub fn image_fit(self, fit: ImageFit) -> View<Msg>
Política de encaje de la imagen (CSS object-fit / Flutter
BoxFit). Solo aplica si hay Self::image seteada. Ver
ImageFit.
Sourcepub fn mask_image(self, image: ImageBrush) -> View<Msg>
pub fn mask_image(self, image: ImageBrush) -> View<Msg>
Aplica image como máscara de luminancia del subárbol del nodo
(CSS mask-image). El paint aísla el subárbol en una capa y multiplica
su alpha por la luminancia de la máscara (blanco = visible, negro =
oculto). La imagen se estira al border-box del nodo. Ortogonal a
Self::image (que pinta contenido) y a los clip_* (que recortan):
un nodo puede llevar máscara y recorte a la vez.
Sourcepub fn mask_placement(self, placement: MaskPlacement) -> View<Msg>
pub fn mask_placement(self, placement: MaskPlacement) -> View<Msg>
Fija el encaje de la máscara (CSS mask-size/-position/-repeat).
Sólo surte efecto junto a Self::mask_image. Sin esto, la máscara se
estira al border-box (Fase 7.1226). Ver MaskPlacement. Fase 7.1227.
Sourcepub fn mask_extra(self, layers: Vec<(ImageBrush, MaskCompose)>) -> View<Msg>
pub fn mask_extra(self, layers: Vec<(ImageBrush, MaskCompose)>) -> View<Msg>
Capas de máscara adicionales (imagen, operador) (CSS mask-image con
lista). Comparten el Self::mask_placement con la capa 0
(Self::mask_image) y se combinan con ella según cada operador. Fase
7.1231.
Sourcepub fn paint_with<F>(self, painter: F) -> View<Msg>
pub fn paint_with<F>(self, painter: F) -> View<Msg>
Registra una closure de pintura custom. El runtime la invoca
con (&mut vello::Scene, &mut Typesetter, PaintRect) durante
el paint del nodo. La closure es responsable de pintar
primitivas custom dentro del rect; no debe dejar push_layer
sin par. Soporte para canvas elements estilo
dominium/pluma/cosmos.
Examples found in repository?
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn gpu_paint_with<F>(self, painter: F) -> View<Msg>where
F: Fn(&Device, &Queue, &mut CommandEncoder, &TextureView, PaintRect, (u32, u32)) + Send + Sync + 'static,
pub fn gpu_paint_with<F>(self, painter: F) -> View<Msg>where
F: Fn(&Device, &Queue, &mut CommandEncoder, &TextureView, PaintRect, (u32, u32)) + Send + Sync + 'static,
Registra una closure de pintura GPU directo. La closure recibe
(&Device, &Queue, &mut CommandEncoder, &TextureView, PaintRect, (viewport_w, viewport_h))
y debe escribir sobre el TextureView con LoadOp::Load (no
clear) para preservar la pasada vello previa. El último
argumento es el tamaño en pixels de la TextureView destino
(la intermedia del frame) — necesario para calcular NDC sin
asumir un viewport fijo. Ver GpuPaintFn para semántica
completa, contexto y orden de pintura.
Examples found in repository?
54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }Sourcepub fn paint_over<F>(self, painter: F) -> View<Msg>
pub fn paint_over<F>(self, painter: F) -> View<Msg>
Registra una closure de pintura vello “over” — misma firma que
Self::paint_with (&mut Scene, &mut Typesetter, PaintRect),
pero el runtime la ejecuta en una pasada vello FINAL después
del pase GPU directo del frame, componiéndola con alpha sobre la
intermedia. Es el complemento opt-in de Self::gpu_paint_with:
permite pintar sprites/texto AA por vello ENCIMA de las celdas
instanciadas por GPU del mismo (o de otro) nodo.
Orden resultante del frame: [vello base] → [gpu_paint] → [paint_over] → [overlay/menús]. Backward-compat total: si nadie
usa paint_over, no se crea la pasada final (coste cero) y el
resto del pipeline es idéntico. La closure no debe dejar
push_layer sin par ni resetear la escena. Ver OverPaintFn.
Sourcepub fn clip(self, enabled: bool) -> View<Msg>
pub fn clip(self, enabled: bool) -> View<Msg>
Recorta los hijos al rect de este nodo (paint y hit-test). Útil para paneles con contenido virtualizado que no debe sangrar a vecinos (listas, scrollers, viewers).
Examples found in repository?
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Sourcepub fn clip_inset(self, insets: [f32; 4]) -> View<Msg>
pub fn clip_inset(self, insets: [f32; 4]) -> View<Msg>
Recorta los descendientes a un rect encogido por insets px
[top, right, bottom, left] desde el rect del nodo — modela
clip-path: inset(...). Activa el recorte (paint + hit-test).
Sourcepub fn clip_ellipse(self, spec: [f32; 14]) -> View<Msg>
pub fn clip_ellipse(self, spec: [f32; 14]) -> View<Msg>
Recorta los descendientes a una elipse — modela
clip-path: circle()/ellipse(). spec es de 14 floats: centro
[cx_px, cx_pct, cy_px, cy_pct] + dos radios [px, pct_w, pct_h, pct_diag, side], todos resueltos contra el rect del nodo en el
pintado. Activa el recorte (paint; hit-test usa el rect completo).
Sourcepub fn clip_polygon(self, evenodd: bool, points: Vec<[f32; 4]>) -> View<Msg>
pub fn clip_polygon(self, evenodd: bool, points: Vec<[f32; 4]>) -> View<Msg>
Recorta los descendientes a un polígono — modela clip-path: polygon(). evenodd = regla de relleno; cada punto [x_px, x_pct, y_px, y_pct] resuelve contra el rect del nodo en el pintado. Activa el
recorte (paint; hit-test usa el rect completo).
Sourcepub fn clip_path_svg(self, evenodd: bool, d: impl Into<String>) -> View<Msg>
pub fn clip_path_svg(self, evenodd: bool, d: impl Into<String>) -> View<Msg>
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. evenodd = regla
de relleno. Activa el recorte (paint; hit-test usa el rect completo).
Sourcepub fn clip_ref_inset(self, insets: [f32; 4]) -> View<Msg>
pub fn clip_ref_inset(self, insets: [f32; 4]) -> View<Msg>
Fija la caja de referencia del clip-path (<geometry-box>): el rect del
nodo se encoge por insets px [top, right, bottom, left] antes de
resolver la forma. Sin forma, recorta a ese rect. Activa el recorte.
Sourcepub fn children(self, children: Vec<View<Msg>>) -> View<Msg>
pub fn children(self, children: Vec<View<Msg>>) -> View<Msg>
Examples found in repository?
70 fn view(model: &Self::Model) -> View<Self::Msg> {
71 let body_text = if model.is_empty() {
72 "tipea algo · ctrl+L limpia · enter salto · backspace borra".to_string()
73 } else {
74 // Cursor visual al final del contenido.
75 format!("{model}\u{2588}")
76 };
77 let body_color = if model.is_empty() {
78 Color::from_rgba8(110, 130, 150, 255)
79 } else {
80 Color::from_rgba8(220, 230, 240, 255)
81 };
82
83 let body = View::new(Style {
84 size: Size {
85 width: percent(1.0_f32),
86 height: percent(1.0_f32),
87 },
88 flex_grow: 1.0,
89 ..Default::default()
90 })
91 .text_aligned(body_text, 22.0, body_color, Alignment::Start);
92
93 let status = View::new(Style {
94 size: Size {
95 width: percent(1.0_f32),
96 height: length(36.0_f32),
97 },
98 ..Default::default()
99 })
100 .fill(Color::from_rgba8(30, 36, 48, 255))
101 .text(
102 format!("{} chars", model.chars().count()),
103 16.0,
104 Color::from_rgba8(160, 180, 200, 255),
105 );
106
107 View::new(Style {
108 flex_direction: FlexDirection::Column,
109 size: Size {
110 width: percent(1.0_f32),
111 height: percent(1.0_f32),
112 },
113 gap: Size {
114 width: length(0.0_f32),
115 height: length(8.0_f32),
116 },
117 padding: llimphi_ui::llimphi_layout::taffy::Rect {
118 left: length(24.0_f32),
119 right: length(24.0_f32),
120 top: length(24.0_f32),
121 bottom: length(24.0_f32),
122 },
123 ..Default::default()
124 })
125 .fill(Color::from_rgba8(20, 24, 32, 255))
126 .children(vec![body, status])
127 }More examples
40 fn view(_: &Self::Model) -> View<Self::Msg> {
41 let mut children: Vec<View<()>> = vec![View::new(Style {
42 size: Size {
43 width: percent(1.0_f32),
44 height: Dimension::auto(),
45 },
46 ..Default::default()
47 })
48 .text_aligned(
49 "Texto seleccionable (arrastrá + Ctrl/Cmd+C)",
50 22.0,
51 Color::from_rgba8(230, 240, 250, 255),
52 Alignment::Start,
53 )];
54
55 for (i, p) in PARRAFOS.iter().enumerate() {
56 children.push(
57 View::new(Style {
58 size: Size {
59 width: percent(1.0_f32),
60 height: Dimension::auto(),
61 },
62 ..Default::default()
63 })
64 .text_aligned(
65 *p,
66 16.0,
67 Color::from_rgba8(205, 214, 226, 255),
68 Alignment::Start,
69 )
70 // La línea clave: cada párrafo es seleccionable con una key
71 // estable (su índice). El resaltado + copy los hace el runtime.
72 .selectable(i as u64),
73 );
74 }
75
76 View::new(Style {
77 flex_direction: FlexDirection::Column,
78 size: Size {
79 width: percent(1.0_f32),
80 height: percent(1.0_f32),
81 },
82 gap: Size {
83 width: length(0.0_f32),
84 height: length(20.0_f32),
85 },
86 padding: Rect {
87 left: length(40.0_f32),
88 right: length(40.0_f32),
89 top: length(36.0_f32),
90 bottom: length(36.0_f32),
91 },
92 align_items: Some(AlignItems::Start),
93 ..Default::default()
94 })
95 .fill(Color::from_rgba8(20, 24, 32, 255))
96 .children(children)
97 }54 fn view(model: &Self::Model) -> View<Self::Msg> {
55 let title = View::new(Style {
56 size: Size {
57 width: percent(1.0_f32),
58 height: length(48.0_f32),
59 },
60 justify_content: Some(JustifyContent::Center),
61 align_items: Some(AlignItems::Center),
62 ..Default::default()
63 })
64 .text(
65 format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66 22.0,
67 Color::from_rgba8(220, 230, 245, 255),
68 );
69
70 // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71 // la grilla de puntos encima vía gpu_paint_with. El seed del
72 // modelo se mete en el shader vía una rotación trivial — cada
73 // click cambia el patrón. El callback se invoca ya con el
74 // CommandEncoder del frame y la TextureView intermediate.
75 let seed = *model;
76 let canvas = View::new(Style {
77 size: Size {
78 width: percent(1.0_f32),
79 height: auto(),
80 },
81 flex_grow: 1.0,
82 ..Default::default()
83 })
84 .fill(Color::from_rgba8(14, 18, 28, 255))
85 .radius(8.0)
86 .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87 draw_points(device, queue, encoder, view, rect, seed);
88 })
89 .on_click(Msg::Bump);
90
91 let footer = View::new(Style {
92 size: Size {
93 width: percent(1.0_f32),
94 height: length(28.0_f32),
95 },
96 justify_content: Some(JustifyContent::Center),
97 align_items: Some(AlignItems::Center),
98 ..Default::default()
99 })
100 .text(
101 "click sobre el canvas → rebobinar el seed",
102 14.0,
103 Color::from_rgba8(150, 165, 185, 255),
104 );
105
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 gap: Size {
113 width: length(0.0_f32),
114 height: length(16.0_f32),
115 },
116 padding: TaffyRect {
117 left: length(24.0_f32),
118 right: length(24.0_f32),
119 top: length(16.0_f32),
120 bottom: length(16.0_f32),
121 },
122 ..Default::default()
123 })
124 .fill(Color::from_rgba8(24, 28, 38, 255))
125 .children(vec![title, canvas, footer])
126 }43 fn view(model: &Self::Model) -> View<Self::Msg> {
44 let number = View::new(Style {
45 size: Size {
46 width: percent(1.0_f32),
47 height: Dimension::auto(),
48 },
49 flex_grow: 1.0,
50 align_items: Some(AlignItems::Center),
51 justify_content: Some(JustifyContent::Center),
52 ..Default::default()
53 })
54 .text(model.to_string(), 160.0, Color::from_rgba8(230, 240, 250, 255));
55
56 let increment = View::new(Style {
57 size: Size {
58 width: length(160.0_f32),
59 height: length(56.0_f32),
60 },
61 align_items: Some(AlignItems::Center),
62 justify_content: Some(JustifyContent::Center),
63 ..Default::default()
64 })
65 .fill(Color::from_rgba8(60, 200, 130, 255))
66 .radius(12.0)
67 .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
68 .on_click(Msg::Increment);
69
70 let reset = View::new(Style {
71 size: Size {
72 width: length(120.0_f32),
73 height: length(56.0_f32),
74 },
75 align_items: Some(AlignItems::Center),
76 justify_content: Some(JustifyContent::Center),
77 ..Default::default()
78 })
79 .fill(Color::from_rgba8(220, 80, 80, 255))
80 .radius(12.0)
81 .text("reset", 22.0, Color::from_rgba8(30, 10, 10, 255))
82 .on_click(Msg::Reset);
83
84 let buttons = View::new(Style {
85 flex_direction: FlexDirection::Row,
86 size: Size {
87 width: percent(1.0_f32),
88 height: length(56.0_f32),
89 },
90 gap: Size {
91 width: length(16.0_f32),
92 height: length(0.0_f32),
93 },
94 justify_content: Some(JustifyContent::Center),
95 ..Default::default()
96 })
97 .children(vec![increment, reset]);
98
99 View::new(Style {
100 flex_direction: FlexDirection::Column,
101 size: Size {
102 width: percent(1.0_f32),
103 height: percent(1.0_f32),
104 },
105 gap: Size {
106 width: length(0.0_f32),
107 height: length(24.0_f32),
108 },
109 padding: llimphi_ui::llimphi_layout::taffy::Rect {
110 left: length(32.0_f32),
111 right: length(32.0_f32),
112 top: length(32.0_f32),
113 bottom: length(32.0_f32),
114 },
115 ..Default::default()
116 })
117 .fill(Color::from_rgba8(20, 24, 32, 255))
118 .children(vec![number, buttons])
119 }101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 // Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
115 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 // Offset del primer línea visible (pan módulo step).
120 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 // Marcas (coords de mundo → pantalla): pan + world·zoom.
136 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 // Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
145 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 // Paneo por arrastre. El movimiento cancela un long-press en curso.
150 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 // Doble-tap: reset. Long-press: marca en el punto.
155 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }Source§impl<Msg> View<Msg>where
Msg: 'static,
impl<Msg> View<Msg>where
Msg: 'static,
Sourcepub fn map<Msg2, F>(self, f: F) -> View<Msg2>
pub fn map<Msg2, F>(self, f: F) -> View<Msg2>
Transforma el Msg de todo el árbol vía f, devolviendo
View<Msg2>. 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.
Auto Trait Implementations§
impl<Msg> !RefUnwindSafe for View<Msg>
impl<Msg> !Send for View<Msg>
impl<Msg> !Sync for View<Msg>
impl<Msg> !UnwindSafe for View<Msg>
impl<Msg> Freeze for View<Msg>where
Msg: Freeze,
impl<Msg> Unpin for View<Msg>where
Msg: Unpin,
impl<Msg> UnsafeUnpin for View<Msg>where
Msg: UnsafeUnpin,
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.