feat: llimphi standalone — framework UI soberano extraído del monorepo
Motor gráfico Llimphi como workspace independiente: bucle Elm (input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley. Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets + módulos, sin dependencias al resto del monorepo. cargo check --workspace pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "llimphi-widget-splitter"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "llimphi-widget-splitter — split container con divisor draggable. Análogo Llimphi al `nahual-widget-splitter` GPUI: dos panes, divisor sólido del ancho del thickness configurable, drag emite Msg con el delta del eje principal."
|
||||
|
||||
[dependencies]
|
||||
llimphi-ui = { workspace = true }
|
||||
llimphi-theme = { workspace = true }
|
||||
@@ -0,0 +1,5 @@
|
||||
# llimphi-widget-splitter
|
||||
|
||||
> Splitter horizontal/vertical para [llimphi](../../README.md).
|
||||
|
||||
Divide el espacio entre dos hijos con un handle arrastrable. Min/max sizes por hijo; doble-click para reset.
|
||||
@@ -0,0 +1,5 @@
|
||||
# llimphi-widget-splitter
|
||||
|
||||
> Horizontal/vertical splitter for [llimphi](../../README.md).
|
||||
|
||||
Divides space between two children with a draggable handle. Per-child min/max sizes; double-click to reset.
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Showcase de `llimphi-widget-splitter`: dos splits anidados
|
||||
//! draggables (Row con Column adentro).
|
||||
//!
|
||||
//! Corré con: `cargo run -p llimphi-widget-splitter --example showcase --release`.
|
||||
//!
|
||||
//! Probá: agarrá el divisor vertical y arrastralo izquierda/derecha
|
||||
//! para resizar el pane izquierdo; agarrá el divisor horizontal de la
|
||||
//! derecha para resizar el pane superior derecho.
|
||||
|
||||
use llimphi_ui::llimphi_layout::taffy::{
|
||||
prelude::{percent, Size, Style},
|
||||
AlignItems, JustifyContent,
|
||||
};
|
||||
use llimphi_ui::llimphi_raster::peniko::Color;
|
||||
use llimphi_ui::llimphi_text::Alignment;
|
||||
use llimphi_ui::{App, DragPhase, Handle, View};
|
||||
use llimphi_widget_splitter::{splitter_two, Direction, PaneSize, SplitterPalette};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Msg {
|
||||
ResizeOuter(f32),
|
||||
ResizeInner(f32),
|
||||
}
|
||||
|
||||
struct Model {
|
||||
left_w: f32,
|
||||
top_h: f32,
|
||||
}
|
||||
|
||||
struct Showcase;
|
||||
|
||||
impl App for Showcase {
|
||||
type Model = Model;
|
||||
type Msg = Msg;
|
||||
|
||||
fn title() -> &'static str {
|
||||
"llimphi · splitter showcase"
|
||||
}
|
||||
|
||||
fn initial_size() -> (u32, u32) {
|
||||
(1100, 720)
|
||||
}
|
||||
|
||||
fn init(_: &Handle<Msg>) -> Model {
|
||||
Model {
|
||||
left_w: 320.0,
|
||||
top_h: 240.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
|
||||
let mut m = model;
|
||||
match msg {
|
||||
Msg::ResizeOuter(dx) => {
|
||||
m.left_w = (m.left_w + dx).clamp(120.0, 800.0);
|
||||
}
|
||||
Msg::ResizeInner(dy) => {
|
||||
m.top_h = (m.top_h + dy).clamp(80.0, 600.0);
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn view(model: &Model) -> View<Msg> {
|
||||
let palette = SplitterPalette::default();
|
||||
|
||||
let left = pane("izquierdo", Color::from_rgba8(28, 36, 50, 255));
|
||||
let top_right = pane(
|
||||
&format!("arriba · {:.0} px", model.top_h),
|
||||
Color::from_rgba8(38, 50, 70, 255),
|
||||
);
|
||||
let bottom_right = pane(
|
||||
"abajo · flex",
|
||||
Color::from_rgba8(48, 36, 60, 255),
|
||||
);
|
||||
|
||||
let right = splitter_two(
|
||||
Direction::Column,
|
||||
top_right,
|
||||
PaneSize::Fixed(model.top_h),
|
||||
bottom_right,
|
||||
PaneSize::Flex,
|
||||
|phase, dy| match phase {
|
||||
DragPhase::Move => Some(Msg::ResizeInner(dy)),
|
||||
DragPhase::End => None,
|
||||
},
|
||||
&palette,
|
||||
);
|
||||
|
||||
splitter_two(
|
||||
Direction::Row,
|
||||
left,
|
||||
PaneSize::Fixed(model.left_w),
|
||||
right,
|
||||
PaneSize::Flex,
|
||||
|phase, dx| match phase {
|
||||
DragPhase::Move => Some(Msg::ResizeOuter(dx)),
|
||||
DragPhase::End => None,
|
||||
},
|
||||
&palette,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn pane(label: &str, bg: Color) -> View<Msg> {
|
||||
View::new(Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: percent(1.0_f32),
|
||||
},
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.fill(bg)
|
||||
.text_aligned(
|
||||
label.to_string(),
|
||||
18.0,
|
||||
Color::from_rgba8(220, 230, 240, 255),
|
||||
Alignment::Center,
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
llimphi_ui::run::<Showcase>();
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! `llimphi-widget-splitter` — split container con divisor draggable.
|
||||
//!
|
||||
//! Análogo Llimphi al `nahual-widget-splitter` GPUI: dos panes con un
|
||||
//! divisor entre medio que el usuario arrastra para reasignar el tamaño.
|
||||
//! El widget no mantiene estado: el caller acumula el tamaño de un pane
|
||||
//! en su `Model` y le pasa el valor actual + un handler `Fn(DragPhase,
|
||||
//! f32) -> Option<Msg>` que materializa el delta en un Msg de update.
|
||||
//!
|
||||
//! Uso típico (dos panes, izquierdo fijo y derecho flex):
|
||||
//!
|
||||
//! ```ignore
|
||||
//! splitter_two(
|
||||
//! Direction::Row,
|
||||
//! left_view,
|
||||
//! PaneSize::Fixed(model.left_size),
|
||||
//! right_view,
|
||||
//! PaneSize::Flex,
|
||||
//! |phase, dx| match phase {
|
||||
//! DragPhase::Move => Some(Msg::ResizeLeft(dx)),
|
||||
//! DragPhase::End => Some(Msg::PersistLayout),
|
||||
//! },
|
||||
//! &SplitterPalette::default(),
|
||||
//! )
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use llimphi_ui::llimphi_layout::taffy::{
|
||||
prelude::{length, percent, Dimension, FlexDirection, Size, Style},
|
||||
Rect,
|
||||
};
|
||||
use llimphi_ui::llimphi_raster::peniko::Color;
|
||||
use llimphi_ui::{DragPhase, View};
|
||||
|
||||
/// Dirección del split. `Row` apila los panes horizontalmente
|
||||
/// (divisor vertical, drag horizontal); `Column` los apila verticalmente.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Row,
|
||||
Column,
|
||||
}
|
||||
|
||||
/// Tamaño de un pane sobre el eje principal del split.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PaneSize {
|
||||
/// Ancho/alto fijo en pixels. El otro pane se ajusta con `flex_grow`.
|
||||
Fixed(f32),
|
||||
/// Toma todo el espacio sobrante (`flex_grow = 1`).
|
||||
Flex,
|
||||
}
|
||||
|
||||
/// Paleta del divisor. Cambia de color al hover para señalar
|
||||
/// "agarrame y arrastrá".
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SplitterPalette {
|
||||
pub divider: Color,
|
||||
pub divider_hover: Color,
|
||||
pub thickness: f32,
|
||||
}
|
||||
|
||||
impl Default for SplitterPalette {
|
||||
fn default() -> Self {
|
||||
Self::from_theme(&llimphi_theme::Theme::dark())
|
||||
}
|
||||
}
|
||||
|
||||
impl SplitterPalette {
|
||||
/// Construye la paleta desde un `Theme` semántico.
|
||||
pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
|
||||
Self {
|
||||
divider: t.border,
|
||||
divider_hover: t.accent,
|
||||
thickness: 6.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split de dos panes con divisor draggable entre medio. `on_resize`
|
||||
/// se invoca con el delta del eje principal (positivo → divisor se
|
||||
/// mueve a la derecha/abajo).
|
||||
pub fn splitter_two<Msg, F>(
|
||||
direction: Direction,
|
||||
a: View<Msg>,
|
||||
a_size: PaneSize,
|
||||
b: View<Msg>,
|
||||
b_size: PaneSize,
|
||||
on_resize: F,
|
||||
palette: &SplitterPalette,
|
||||
) -> View<Msg>
|
||||
where
|
||||
Msg: Clone + Send + Sync + 'static,
|
||||
F: Fn(DragPhase, f32) -> Option<Msg> + Send + Sync + 'static,
|
||||
{
|
||||
let flex_dir = match direction {
|
||||
Direction::Row => FlexDirection::Row,
|
||||
Direction::Column => FlexDirection::Column,
|
||||
};
|
||||
|
||||
// El divisor sólo necesita Msg en el eje principal — escondemos el
|
||||
// otro detrás del closure.
|
||||
let on_resize = Arc::new(on_resize);
|
||||
let cb_dir = direction;
|
||||
let cb = on_resize.clone();
|
||||
let divider = divider_view::<Msg>(direction, palette, move |phase, dx, dy| {
|
||||
let main = match cb_dir {
|
||||
Direction::Row => dx,
|
||||
Direction::Column => dy,
|
||||
};
|
||||
(cb)(phase, main)
|
||||
});
|
||||
|
||||
let pane_a = wrap_pane(a, direction, a_size);
|
||||
let pane_b = wrap_pane(b, direction, b_size);
|
||||
|
||||
View::new(Style {
|
||||
flex_direction: flex_dir,
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: percent(1.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.children(vec![pane_a, divider, pane_b])
|
||||
}
|
||||
|
||||
fn wrap_pane<Msg>(view: View<Msg>, direction: Direction, size: PaneSize) -> View<Msg> {
|
||||
let (width, height, flex_grow) = match (direction, size) {
|
||||
(Direction::Row, PaneSize::Fixed(px)) => (length(px), percent(1.0_f32), 0.0),
|
||||
(Direction::Row, PaneSize::Flex) => (Dimension::auto(), percent(1.0_f32), 1.0),
|
||||
(Direction::Column, PaneSize::Fixed(px)) => (percent(1.0_f32), length(px), 0.0),
|
||||
(Direction::Column, PaneSize::Flex) => (percent(1.0_f32), Dimension::auto(), 1.0),
|
||||
};
|
||||
View::new(Style {
|
||||
size: Size { width, height },
|
||||
flex_grow,
|
||||
flex_shrink: 0.0,
|
||||
min_size: Size {
|
||||
width: length(0.0_f32),
|
||||
height: length(0.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.children(vec![view])
|
||||
}
|
||||
|
||||
fn divider_view<Msg>(
|
||||
direction: Direction,
|
||||
palette: &SplitterPalette,
|
||||
handler: impl Fn(DragPhase, f32, f32) -> Option<Msg> + Send + Sync + 'static,
|
||||
) -> View<Msg>
|
||||
where
|
||||
Msg: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let (width, height) = match direction {
|
||||
Direction::Row => (length(palette.thickness), percent(1.0_f32)),
|
||||
Direction::Column => (percent(1.0_f32), length(palette.thickness)),
|
||||
};
|
||||
View::new(Style {
|
||||
size: Size { width, height },
|
||||
flex_shrink: 0.0,
|
||||
padding: Rect {
|
||||
left: length(0.0_f32),
|
||||
right: length(0.0_f32),
|
||||
top: length(0.0_f32),
|
||||
bottom: length(0.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.fill(palette.divider)
|
||||
.hover_fill(palette.divider_hover)
|
||||
.draggable(handler)
|
||||
}
|
||||
Reference in New Issue
Block a user