tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4
widgets/dock-rail/src/lib.rs
History
Who touched what
raw
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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}