tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4 widgets/dock-rail/src/lib.rs Rust · 190 lines · 6.4 KB History Who touched what raw

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

1//! `llimphi-widget-dock-rail` — rail vertical de **dientes** para
2//! sidebars acoplables.
3//!
4//! Cada diente es una pestaña vertical: una **barra de acento** de 3px
5//! pegada al borde interno (encendida cuando el item está activo) + un
6//! **icono** centrado. Los dientes apilan en una columna redondeada que
7//! se pinta como una franja flotante al borde del centro — el patrón del
8//! dock de cosmos, ahora reutilizable.
9//!
10//! Render-only y agnóstico del `Msg`: el item se identifica por un `u64`
11//! opaco. El clic (en el *press*, para no pelear con el arrastre) activa
12//! vía `on_activate(id)`; el diente es **arrastrable** con su `id` como
13//! payload, y el rail entero es **drop target** (`on_drop(payload)`) —
14//! así una app puede mover un diente de un sidebar a otro soltándolo
15//! sobre el rail opuesto. El icono lo dibuja el caller (`make_icon`), que
16//! recibe el color ya resuelto según el estado activo/inactivo.
17//!
18//! ```ignore
19//! let items = [DockRailItem { id: 0, active: true }, DockRailItem { id: 1, active: false }];
20//! dock_rail_view(
21//!     &items,
22//!     44.0,
23//!     &DockRailPalette::from_theme(&theme),
24//!     |id, size, color| my_icon_view(id, size, color),
25//!     |id| Msg::DockActivate(side, id),
26//!     move |payload| Some(Msg::DockDrop(side, payload)),
27//! )
28//! ```
29
30#![forbid(unsafe_code)]
31
32use llimphi_ui::llimphi_layout::taffy::{
33    prelude::{auto, length, percent, FlexDirection, Size, Style},
34    AlignItems, JustifyContent,
35};
36use llimphi_ui::llimphi_raster::peniko::Color;
37use llimphi_ui::{DragPhase, View};
38use llimphi_theme::Theme;
39
40/// Alto de cada diente (px).
41const TOOTH_H: f32 = 42.0;
42/// Alto de la barra de acento (px) — un poco menor que el diente para
43/// dejar aire arriba y abajo.
44const BAR_H: f32 = 40.0;
45/// Tamaño del icono que se le pide al caller (px).
46const ICON_PX: f32 = 20.0;
47
48/// Paleta del rail.
49#[derive(Debug, Clone, Copy)]
50pub struct DockRailPalette {
51    /// Fondo de la franja del rail.
52    pub bg_rail: Color,
53    /// Fondo del diente activo.
54    pub bg_active: Color,
55    /// Fondo al hover (diente) y al sobrevolar un drop válido (rail).
56    pub bg_hover: Color,
57    /// Color del acento: barra del diente activo + su icono.
58    pub accent: Color,
59    /// Color del icono de un diente inactivo.
60    pub icon_inactive: Color,
61}
62
63impl DockRailPalette {
64    pub fn from_theme(t: &Theme) -> Self {
65        Self {
66            bg_rail: t.bg_panel_alt,
67            bg_active: t.bg_selected,
68            bg_hover: t.bg_row_hover,
69            accent: t.accent,
70            icon_inactive: t.fg_muted,
71        }
72    }
73}
74
75/// Un diente del rail: su id opaco y si está activo.
76#[derive(Debug, Clone, Copy)]
77pub struct DockRailItem {
78    pub id: u64,
79    pub active: bool,
80}
81
82/// Construye el rail de dientes.
83///
84/// - `items`: en el orden en que se quieren mostrar (el widget no
85///   reordena).
86/// - `width`: ancho de la franja del rail (px).
87/// - `make_icon(id, size, color)`: dibuja el icono del item con el color
88///   ya resuelto (acento si activo, atenuado si no).
89/// - `on_activate(id)`: `Msg` al clickear (en el press).
90/// - `on_drop(payload)`: `Msg` opcional cuando se suelta un diente
91///   (cualquier `id`) sobre este rail.
92pub fn dock_rail_view<Msg, FIcon, FAct, FDrop>(
93    items: &[DockRailItem],
94    width: f32,
95    palette: &DockRailPalette,
96    make_icon: FIcon,
97    on_activate: FAct,
98    on_drop: FDrop,
99) -> View<Msg>
100where
101    Msg: Clone + Send + Sync + 'static,
102    FIcon: Fn(u64, f32, Color) -> View<Msg>,
103    FAct: Fn(u64) -> Msg,
104    FDrop: Fn(u64) -> Option<Msg> + Send + Sync + 'static,
105{
106    let mut teeth: Vec<View<Msg>> = Vec::with_capacity(items.len());
107    for item in items {
108        let fg = if item.active {
109            palette.accent
110        } else {
111            palette.icon_inactive
112        };
113        // Barra de acento, pegada al borde interno.
114        let accent_bar = {
115            let b = View::new(Style {
116                size: Size {
117                    width: length(3.0_f32),
118                    height: length(BAR_H),
119                },
120                flex_shrink: 0.0,
121                ..Default::default()
122            });
123            if item.active {
124                b.fill(palette.accent).radius(2.0)
125            } else {
126                b
127            }
128        };
129        let icon_box = View::new(Style {
130            flex_grow: 1.0,
131            size: Size {
132                width: percent(0.0_f32),
133                height: length(TOOTH_H),
134            },
135            align_items: Some(AlignItems::Center),
136            justify_content: Some(JustifyContent::Center),
137            ..Default::default()
138        })
139        .children(vec![make_icon(item.id, ICON_PX, fg)]);
140
141        let id = item.id;
142        let msg = on_activate(id);
143        let mut tooth = View::new(Style {
144            flex_direction: FlexDirection::Row,
145            size: Size {
146                width: percent(1.0_f32),
147                height: length(TOOTH_H),
148            },
149            flex_shrink: 0.0,
150            align_items: Some(AlignItems::Center),
151            ..Default::default()
152        })
153        .hover_fill(palette.bg_hover)
154        // Click en el press activa; arrastrar mueve de rail (payload=id).
155        .on_click_at(move |_, _, _, _| Some(msg.clone()))
156        .draggable_at(|phase, _, _, _, _| match phase {
157            DragPhase::Move | DragPhase::End => None,
158        })
159        .drag_payload(id)
160        .children(vec![accent_bar, icon_box]);
161        if item.active {
162            // Pestaña activa: además del fill, redondea el lado que sobresale
163            // hacia el contenido (la barra de acento marca el borde interno a
164            // la izquierda, así que el diente abre a la derecha). Le da el look
165            // de pestaña que sale del rail en vez de un rectángulo plano.
166            tooth = tooth
167                .fill(palette.bg_active)
168                .radius_corners(0.0, 8.0, 8.0, 0.0);
169        }
170        teeth.push(tooth);
171    }
172
173    // La franja: sólo del alto de los dientes (el hueco de abajo lo deja
174    // libre para que el área central lo aproveche si el rail flota como
175    // overlay). Es además el drop target del lado.
176    View::new(Style {
177        flex_direction: FlexDirection::Column,
178        size: Size {
179            width: length(width),
180            height: auto(),
181        },
182        flex_shrink: 0.0,
183        ..Default::default()
184    })
185    .fill(palette.bg_rail)
186    .radius(5.0)
187    .on_drop(on_drop)
188    .drop_hover_fill(palette.bg_hover)
189    .children(teeth)
190}