feat(tahuantinsuyu): UX pass — splitter, light wheel, scroll, zoom/pan, dock lateral
Seis fixes derivados de testing real, ordenados por costo:
- Splitter (yahweh-widget-splitter): `flex-basis: 0` por item para que
el ratio flex-grow se respete sin importar el min-content de los
hijos. Sin esto, al cambiar el canvas de Empty→Wheel (WHEEL_SIZE
fijo de 580px) la suma de basis excedía el contenedor y flexbox
abandonaba el ratio 1:4, aplastando el tree a 0px (síntoma
reportado: "el tree desaparece al seleccionar carta"). También se
amplió la hit-zone del divider de 4px a 12px manteniendo una franja
visual de 4px centrada — la zona de pointer-capture y cursor es
ahora mucho más generosa, el visual sigue fino.
- Light mode wheel (tahuantinsuyu-canvas + tahuantinsuyu-theme): el
gradient del fondo del wheel pasa de alphas 0.06/0.03 (invisibles
contra fondo claro) a 0.18/0.10 cuando el theme es light. Cusps y
aspectos secundarios del light palette bajan luminancia y suben
alpha para no lavarse contra blanco.
- Panel scroll (tahuantinsuyu-panel): body del control panel agrega
`flex_grow + min_h(0) + overflow_y_scroll` para que cuando los
controles no caben aparezca scroll vertical en lugar de cortarse.
- Canvas zoom + pan (tahuantinsuyu-canvas): nuevo estado
view_scale / view_pan_x / view_pan_y. Ctrl+wheel zoomea
multiplicativo (clamp 0.5..3.0); wheel solo paneja. MMB drag para
pan libre. Hotkey `0` resetea zoom+pan. Hit-tests del jog-dial y
hover derivan ahora el `r_outer` del width actual del canvas, así
se autoescalan con el zoom.
- Panel dock lateral (shell.rs): nuevo `PanelDock { Bottom, Right,
Left }` configurable desde 3 botones en el header (◧ ▭ ◨). Bottom
mantiene el layout histórico (tree+canvas / panel); las variantes
laterales colapsan los splitters anidados en uno solo horizontal
de 3 columnas. El dock se persiste en `layout.panel_dock` y cada
layout guarda sus flex en una key distinta para no pisarse.
`load_split_flex_n` / `save_split_flex` generalizados a N hijos.
Tests: 6 pasan (incluye nuevo roundtrip de PanelDock y N-flex).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -119,7 +119,7 @@ impl SplitContainer {
|
||||
// Restamos el espacio que ocupan los divisores — son fixed-size en el
|
||||
// eje principal, no participan del flex. El "espacio disponible
|
||||
// para flex" es lo que importa para convertir delta_px → delta_flex.
|
||||
let dividers_total = px(DIVIDER_THICKNESS) * (self.children.len().saturating_sub(1) as f32);
|
||||
let dividers_total = px(DIVIDER_HIT_ZONE) * (self.children.len().saturating_sub(1) as f32);
|
||||
let total_main = raw_main - dividers_total;
|
||||
if total_main <= px(0.0) {
|
||||
return;
|
||||
@@ -210,7 +210,12 @@ fn main_axis_pt(dir: LayoutDirection, p: Point<Pixels>) -> Pixels {
|
||||
// Render
|
||||
// =====================================================================
|
||||
|
||||
const DIVIDER_THICKNESS: f32 = 4.0;
|
||||
/// Espesor visible de la franja del divisor (la barrita coloreada).
|
||||
const DIVIDER_VISUAL: f32 = 4.0;
|
||||
/// Espesor total de la zona interactiva: cursor + handlers de mouse. Más
|
||||
/// generosa que el visual para no pelearse con el usuario al apuntar a
|
||||
/// una banda de 4px. El visual queda centrado dentro del hit zone.
|
||||
const DIVIDER_HIT_ZONE: f32 = 12.0;
|
||||
|
||||
impl Render for SplitContainer {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -260,13 +265,21 @@ impl Render for SplitContainer {
|
||||
item.style().flex_grow = Some(weight);
|
||||
item.style().flex_shrink = Some(1.0);
|
||||
|
||||
// CRUCIAL: el default de flexbox es `min-width: auto` (= min
|
||||
// content size). Si no lo aplastamos a 0, taffy clamp-ea al
|
||||
// tamaño mínimo del contenido (un TreeView con label largo, un
|
||||
// uniform_list, etc.) y el divisor no puede pasar de ese punto
|
||||
// — el cursor avanza pero el divisor se queda. Forzando min=0
|
||||
// y overflow:hidden en el wrapper, el child puede shrink-arse a
|
||||
// donde sea y el contenido se recorta.
|
||||
// CRUCIAL: flex-basis = 0 (no `auto`). El default `auto` toma
|
||||
// el min-content de cada hijo como punto de partida; cuando un
|
||||
// hijo tiene contenido grande (canvas con WHEEL_SIZE fijo, un
|
||||
// panel con muchos controles en flex_wrap, etc.) la suma de
|
||||
// bases excede el contenedor y flexbox abandona el reparto
|
||||
// por flex-grow para usar shrink proporcional a la basis —
|
||||
// resultado: el ratio 1:4 que pide el host se ignora y el
|
||||
// hijo más liviano (p. ej. el tree) se aplasta a 0px. Con
|
||||
// basis=0 todo el espacio es "free space" y el ratio se
|
||||
// respeta sin importar el contenido.
|
||||
item.style().flex_basis = Some(Length::Definite(px(0.0).into()));
|
||||
|
||||
// Floor de shrink: con basis=0 esto rara vez importa, pero lo
|
||||
// dejamos por defensa contra contenidos que fuercen min-size
|
||||
// intrínseco (uniform_list mide su primera row, etc.).
|
||||
item.style().min_size.width = Some(Length::Definite(px(0.0).into()));
|
||||
item.style().min_size.height = Some(Length::Definite(px(0.0).into()));
|
||||
|
||||
@@ -285,27 +298,46 @@ impl Render for SplitContainer {
|
||||
let divider_idx = i;
|
||||
let entity_for_canvas = entity.clone();
|
||||
|
||||
let mut divider = div();
|
||||
let divider_bg = if self.drag.as_ref().map(|d| d.divider_index) == Some(divider_idx)
|
||||
{
|
||||
let is_active = self.drag.as_ref().map(|d| d.divider_index) == Some(divider_idx);
|
||||
let visual_bg = if is_active {
|
||||
theme.accent_strong
|
||||
} else {
|
||||
theme.border_strong
|
||||
};
|
||||
divider = divider.bg(divider_bg).hover(|s| s.bg(theme.accent));
|
||||
|
||||
// Visual: la franja fina coloreada que el usuario ve.
|
||||
let visual = match direction {
|
||||
LayoutDirection::Horizontal => div()
|
||||
.w(px(DIVIDER_VISUAL))
|
||||
.h_full()
|
||||
.bg(visual_bg),
|
||||
_ => div()
|
||||
.w_full()
|
||||
.h(px(DIVIDER_VISUAL))
|
||||
.bg(visual_bg),
|
||||
};
|
||||
|
||||
// Hit zone: wrapper transparente más ancho que captura
|
||||
// cursor y handlers de mouse. Centra el visual con flex.
|
||||
// `relative` para que el canvas hijo (absolute) se ancle
|
||||
// al wrapper y reporte sus bounds correctos.
|
||||
let mut divider = div().relative().flex().items_center().justify_center();
|
||||
divider = match direction {
|
||||
LayoutDirection::Horizontal => divider
|
||||
.w(px(DIVIDER_THICKNESS))
|
||||
.w(px(DIVIDER_HIT_ZONE))
|
||||
.h_full()
|
||||
.cursor_ew_resize(),
|
||||
_ => divider
|
||||
.w_full()
|
||||
.h(px(DIVIDER_THICKNESS))
|
||||
.h(px(DIVIDER_HIT_ZONE))
|
||||
.cursor_ns_resize(),
|
||||
};
|
||||
divider = divider.child(visual);
|
||||
|
||||
// Canvas con handlers de drag a nivel de window.
|
||||
// Canvas con handlers de drag a nivel de window — su
|
||||
// bounds = bounds del wrapper (hit zone completo), así
|
||||
// que el `canvas_bounds.contains` acepta clicks en todo
|
||||
// el ancho del hit zone, no solo sobre el visual.
|
||||
let divider = divider.child(
|
||||
canvas(
|
||||
|_, _, _| (),
|
||||
@@ -350,6 +382,7 @@ impl Render for SplitContainer {
|
||||
});
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.size_full(),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user