feat(shuma-shell): divisores arrastrables entre los paneles
Cada panel lateral tiene ahora un divisor de 5px que se arrastra para
redimensionarlo (cursor resize, resaltado al hover). El arrastre se
sigue a nivel de ventana —on_mouse_move/up en la raíz— así que no se
pierde aunque el cursor salga del divisor. Ancho acotado 130–420px;
los divisores desaparecen con el panel colapsado.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
90607bd7c06a63b6047b5409d1187276ad2745c2 · por sergio · 2026-05-20T18:39:53Z (hace 5 meses) · padre 2b2a92a72 · ver el árbol en este commit
Delta semántico · 5
crates/apps/shuma-shell/src/main.rs
nueva struct Drag cambió de firma struct Shell nueva enum Side cambió por dentro fn new cambió por dentro fn render
diff --git a/crates/apps/shuma-shell/src/main.rs b/crates/apps/shuma-shell/src/main.rs
index 2531f7e..d94cd76 100644
--- a/crates/apps/shuma-shell/src/main.rs
+++ b/crates/apps/shuma-shell/src/main.rs
@@ -20,9 +20,10 @@ use std::panic;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use gpui::{
- div, point, prelude::*, px, App, Bounds, Context, Element, ElementId, FocusHandle,
- GlobalElementId, Hsla, InspectorElementId, IntoElement, KeyDownEvent, LayoutId, PathBuilder,
- Pixels, Render, SharedString, Style, Window,
+ div, point, prelude::*, px, App, Bounds, Context, CursorStyle, Element, ElementId, FocusHandle,
+ GlobalElementId, Hsla, InspectorElementId, IntoElement, KeyDownEvent, LayoutId, MouseButton,
+ MouseDownEvent, MouseMoveEvent, MouseUpEvent, PathBuilder, Pixels, Render, SharedString, Style,
+ Window,
};
use nahual_launcher::launch_app;
use nahual_theme::Theme;
@@ -201,6 +202,22 @@ impl Element for CurveElement {
// El shell.
// =====================================================================
+/// Qué panel lateral está redimensionando un drag activo.
+#[derive(Clone, Copy)]
+enum Side {
+ Left,
+ Right,
+}
+
+/// Estado de un arrastre de divisor en curso.
+struct Drag {
+ side: Side,
+ /// Posición X del cursor al iniciar el arrastre.
+ start_x: f32,
+ /// Ancho del panel al iniciar el arrastre.
+ start_w: f32,
+}
+
struct Shell {
line: LineState,
/// La sesión de trabajo: cwd, historial y grupos.
@@ -215,6 +232,11 @@ struct Shell {
snapshot: Snapshot,
left_collapsed: bool,
right_collapsed: bool,
+ /// Anchos de los paneles laterales (los divisores los ajustan).
+ left_width: f32,
+ right_width: f32,
+ /// Arrastre de divisor en curso, si lo hay.
+ drag: Option<Drag>,
focus: FocusHandle,
focused_once: bool,
}
@@ -248,6 +270,9 @@ impl Shell {
},
left_collapsed: false,
right_collapsed: false,
+ left_width: 176.0,
+ right_width: 188.0,
+ drag: None,
focus: cx.focus_handle(),
focused_once: false,
};
@@ -687,7 +712,7 @@ impl Render for Shell {
.collect();
div()
.id("run-panel")
- .w(px(176.))
+ .w(px(self.left_width))
.flex()
.flex_col()
.gap(px(6.))
@@ -796,7 +821,7 @@ impl Render for Shell {
div()
.id("sens-panel")
- .w(px(184.))
+ .w(px(self.right_width))
.flex()
.flex_col()
.gap(px(10.))
@@ -949,6 +974,40 @@ impl Render for Shell {
}
}
+ // --- Divisores arrastrables ---
+ let divider = |side: Side, cx: &mut Context<Self>| {
+ div()
+ .w(px(5.))
+ .bg(node_bg)
+ .cursor(CursorStyle::ResizeLeftRight)
+ .hover(|s| s.bg(accent))
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |shell, ev: &MouseDownEvent, _w, cx| {
+ let start_w = match side {
+ Side::Left => shell.left_width,
+ Side::Right => shell.right_width,
+ };
+ shell.drag = Some(Drag {
+ side,
+ start_x: ev.position.x.into(),
+ start_w,
+ });
+ cx.notify();
+ }),
+ )
+ };
+
+ let mut middle = div().flex().flex_row().flex_1().overflow_hidden().child(left);
+ if !self.left_collapsed {
+ middle = middle.child(divider(Side::Left, cx));
+ }
+ middle = middle.child(canvas);
+ if !self.right_collapsed {
+ middle = middle.child(divider(Side::Right, cx));
+ }
+ middle = middle.child(right);
+
// --- Composición ---
div()
.size_full()
@@ -959,17 +1018,33 @@ impl Render for Shell {
.track_focus(&self.focus)
.key_context("ShumaShell")
.on_key_down(cx.listener(Self::handle_key))
- .child(status)
- .child(
- div()
- .flex()
- .flex_row()
- .flex_1()
- .overflow_hidden()
- .child(left)
- .child(canvas)
- .child(right),
+ .on_mouse_move(cx.listener(|shell, ev: &MouseMoveEvent, _w, cx| {
+ if let Some(drag) = &shell.drag {
+ let cur: f32 = ev.position.x.into();
+ let delta = cur - drag.start_x;
+ match drag.side {
+ // El panel izquierdo crece al arrastrar a la derecha.
+ Side::Left => {
+ shell.left_width = (drag.start_w + delta).clamp(130.0, 420.0)
+ }
+ // El derecho crece al arrastrar a la izquierda.
+ Side::Right => {
+ shell.right_width = (drag.start_w - delta).clamp(130.0, 420.0)
+ }
+ }
+ cx.notify();
+ }
+ }))
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(|shell, _ev: &MouseUpEvent, _w, cx| {
+ if shell.drag.take().is_some() {
+ cx.notify();
+ }
+ }),
)
+ .child(status)
+ .child(middle)
.child(prompt)
.children(popup_layer)
}