Files
llimphi/widgets/switch/src/lib.rs
T
Sergio ccab39f140 refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la
estructura vieja de eventloop) y suma el 3D:

- bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 /
  accesskit_winit 0.33 / vello_hybrid 0.0.9.
- nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido,
  montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel
  (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox).
- README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif).
- excluido modules/allichay (arrastra deps fuera del alcance del front-door).
- cargo check --workspace: verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:40:00 +00:00

158 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `llimphi-widget-switch` — toggle binario (track + thumb).
//!
//! Render-only: la app guarda el `bool` en su modelo y dispatcha el
//! Msg de toggle al click. Visualmente:
//! - Track horizontal (40×22 default) con color del estado activo.
//! - Thumb circular (18px) que se posiciona a la izquierda (off) o
//! derecha (on) del track.
//!
//! Para animar la transición, la app puede guardar un `Tween<f32>` con
//! el progreso 0→1 y leerlo desde `view` para interpolar la posición
//! del thumb. Sin tween la transición es instantánea — funcional pero
//! menos elegante.
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{auto, length, Position, Size, Style},
Rect,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::View;
use llimphi_theme::Theme;
/// Paleta del switch.
#[derive(Debug, Clone, Copy)]
pub struct SwitchPalette {
pub track_off: Color,
pub track_on: Color,
pub thumb: Color,
}
impl SwitchPalette {
pub fn from_theme(t: &Theme) -> Self {
Self {
track_off: t.bg_button,
track_on: t.accent,
thumb: t.fg_text,
}
}
}
const TRACK_W: f32 = 40.0;
const TRACK_H: f32 = 22.0;
const THUMB_R: f32 = 9.0; // radio en px → diámetro 18
const PAD: f32 = 2.0;
/// Construye un switch. `progress` en `[0.0, 1.0]` indica la
/// posición animada del thumb (0 = off, 1 = on). Para la transición
/// instantánea usar `if state { 1.0 } else { 0.0 }`.
///
/// `on_toggle` se dispatcha al click; la app actualiza su `bool` y
/// (opcionalmente) lanza un `Tween` que actualiza `progress` por frame.
pub fn switch_view<Msg: Clone + 'static>(
progress: f32,
on_toggle: Msg,
palette: &SwitchPalette,
) -> View<Msg> {
let p = progress.clamp(0.0, 1.0);
// Track color interpola entre off y on según progress.
let track_color = lerp_color(palette.track_off, palette.track_on, p);
// Thumb absolute dentro del track. Range del centro: PAD+THUMB_R a TRACK_W-PAD-THUMB_R.
let min_x = PAD;
let max_x = TRACK_W - PAD - THUMB_R * 2.0;
let thumb_x = min_x + (max_x - min_x) * p;
let thumb_y = (TRACK_H - THUMB_R * 2.0) * 0.5;
let thumb = View::new(Style {
position: Position::Absolute,
inset: Rect {
left: length(thumb_x),
top: length(thumb_y),
right: auto(),
bottom: auto(),
},
size: Size {
width: length(THUMB_R * 2.0),
height: length(THUMB_R * 2.0),
},
..Default::default()
})
.fill(palette.thumb)
.radius(THUMB_R as f64)
.paint_with(move |scene, _ts, rect| {
// Highlight radial pequeño en cuadrante superior — el thumb se
// lee como esfera, no como círculo plano. Mismo patrón que el
// dot del badge (P6).
use llimphi_ui::llimphi_raster::kurbo::{Affine, Circle};
use llimphi_ui::llimphi_raster::peniko::Fill;
if rect.w <= 0.0 || rect.h <= 0.0 {
return;
}
let cx = (rect.x + rect.w * 0.5) as f64;
let cy = (rect.y + rect.h * 0.32) as f64;
let r = (rect.w as f64 * 0.18).max(1.0);
let highlight = Color::from_rgba8(255, 255, 255, 70);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
highlight,
None,
&Circle::new((cx, cy), r),
);
});
let track_radius = (TRACK_H * 0.5) as f64;
View::new(Style {
size: Size {
width: length(TRACK_W),
height: length(TRACK_H),
},
..Default::default()
})
.fill(track_color)
.radius(track_radius)
.paint_with(move |scene, _ts, rect| {
// Gloss superior en el track — pill con luz cayendo desde arriba.
// El track interpola color (off/on) en el fill, el gloss queda
// estable encima en ambos estados.
use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
if rect.w <= 0.0 || rect.h <= 0.0 {
return;
}
let x0 = rect.x as f64;
let y0 = rect.y as f64;
let x1 = (rect.x + rect.w) as f64;
let y1 = (rect.y + rect.h) as f64;
let y_mid = y0 + (y1 - y0) * 0.5;
let rr = RoundedRect::new(x0, y0, x1, y1, track_radius);
let top = Color::from_rgba8(255, 255, 255, 28);
let bot = Color::from_rgba8(255, 255, 255, 0);
let g = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y_mid))
.with_stops([top, bot].as_slice());
scene.fill(Fill::NonZero, Affine::IDENTITY, &g, None, &rr);
})
// Semántica: track switch (Checkbox con `pressed` para que el lector
// diga "interruptor on/off" en vez de "casilla marcada"). Sin un rol
// dedicado en accesskit; Button + pressed cubre la intención.
.role(llimphi_ui::Role::Button)
.aria_pressed(p >= 0.5)
.on_click(on_toggle)
.children(vec![thumb])
}
fn lerp_color(a: Color, b: Color, t: f32) -> Color {
let [r0, g0, b0, a0] = a.components;
let [r1, g1, b1, a1] = b.components;
use llimphi_ui::llimphi_raster::peniko::color::AlphaColor;
AlphaColor::new([
r0 + (r1 - r0) * t,
g0 + (g1 - g0) * t,
b0 + (b1 - b0) * t,
a0 + (a1 - a0) * t,
])
}