e65e9cc623
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>
133 lines
3.8 KiB
Rust
133 lines
3.8 KiB
Rust
//! Editor mínimo: text field con char insertion, backspace, enter, ctrl+L
|
|
//! para limpiar. Valida que el bucle Elm absorbe input de teclado.
|
|
//!
|
|
//! Corre con: `cargo run -p llimphi-ui --example editor --release`.
|
|
|
|
use llimphi_ui::llimphi_layout::taffy::{
|
|
prelude::{length, percent, FlexDirection, Size, Style},
|
|
};
|
|
use llimphi_ui::llimphi_raster::peniko::Color;
|
|
use llimphi_ui::llimphi_text::Alignment;
|
|
use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, NamedKey, View};
|
|
|
|
#[derive(Clone)]
|
|
enum Msg {
|
|
Insert(String),
|
|
Backspace,
|
|
Clear,
|
|
}
|
|
|
|
struct Editor;
|
|
|
|
impl App for Editor {
|
|
type Model = String;
|
|
type Msg = Msg;
|
|
|
|
fn title() -> &'static str {
|
|
"llimphi · editor"
|
|
}
|
|
|
|
fn init(_: &Handle<Self::Msg>) -> Self::Model {
|
|
String::new()
|
|
}
|
|
|
|
fn update(model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
|
|
match msg {
|
|
Msg::Insert(s) => {
|
|
let mut m = model;
|
|
m.push_str(&s);
|
|
m
|
|
}
|
|
Msg::Backspace => {
|
|
let mut m = model;
|
|
m.pop();
|
|
m
|
|
}
|
|
Msg::Clear => String::new(),
|
|
}
|
|
}
|
|
|
|
fn on_key(_: &Self::Model, e: &KeyEvent) -> Option<Self::Msg> {
|
|
if e.state != KeyState::Pressed {
|
|
return None;
|
|
}
|
|
if e.modifiers.ctrl {
|
|
if let Key::Character(c) = &e.key {
|
|
if c.eq_ignore_ascii_case("l") {
|
|
return Some(Msg::Clear);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
match &e.key {
|
|
Key::Named(NamedKey::Backspace) => Some(Msg::Backspace),
|
|
Key::Named(NamedKey::Enter) => Some(Msg::Insert("\n".into())),
|
|
Key::Named(NamedKey::Tab) => Some(Msg::Insert(" ".into())),
|
|
_ => e.text.clone().map(Msg::Insert),
|
|
}
|
|
}
|
|
|
|
fn view(model: &Self::Model) -> View<Self::Msg> {
|
|
let body_text = if model.is_empty() {
|
|
"tipea algo · ctrl+L limpia · enter salto · backspace borra".to_string()
|
|
} else {
|
|
// Cursor visual al final del contenido.
|
|
format!("{model}\u{2588}")
|
|
};
|
|
let body_color = if model.is_empty() {
|
|
Color::from_rgba8(110, 130, 150, 255)
|
|
} else {
|
|
Color::from_rgba8(220, 230, 240, 255)
|
|
};
|
|
|
|
let body = View::new(Style {
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: percent(1.0_f32),
|
|
},
|
|
flex_grow: 1.0,
|
|
..Default::default()
|
|
})
|
|
.text_aligned(body_text, 22.0, body_color, Alignment::Start);
|
|
|
|
let status = View::new(Style {
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: length(36.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.fill(Color::from_rgba8(30, 36, 48, 255))
|
|
.text(
|
|
format!("{} chars", model.chars().count()),
|
|
16.0,
|
|
Color::from_rgba8(160, 180, 200, 255),
|
|
);
|
|
|
|
View::new(Style {
|
|
flex_direction: FlexDirection::Column,
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: percent(1.0_f32),
|
|
},
|
|
gap: Size {
|
|
width: length(0.0_f32),
|
|
height: length(8.0_f32),
|
|
},
|
|
padding: llimphi_ui::llimphi_layout::taffy::Rect {
|
|
left: length(24.0_f32),
|
|
right: length(24.0_f32),
|
|
top: length(24.0_f32),
|
|
bottom: length(24.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.fill(Color::from_rgba8(20, 24, 32, 255))
|
|
.children(vec![body, status])
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
llimphi_ui::run::<Editor>();
|
|
}
|