feat(yahweh): caret blinking + slots ornament en theme + MetaApp full themed
Iters 8-9 combinadas. Tres mejoras pequeñas que cierran la integración del theme: 1) text-input caret blinking: caret_visible bool toggea cada 500ms via cx.spawn loop. _blink_task se mantiene en self para que drop cancele. render dibuja | sólo si focused && caret_visible. 2) yahweh-theme: 5 slots ornament secundario como methods (no fields) derivados de is_dark via ornament_slots() helper: bg_input/bg_button/bg_button_hover/accent_destructive/ bg_destructive_hover. No requiere modificar los 6 presets. 3) MetaApp ornament cleanup: 11 rgb(0x...) hardcoded → slots del theme. Sidebar menu items, list row separator/buttons, icon ✕ delete y su hover, EntityRef selector hovers, form submit button + fallback input bg, confirm modal hint y hovers. Pattern: let X = theme.slot() antes de las closures + move |d| d.bg(X) en hover/when para tomar ownership. Antes MetaApp tenía la paleta principal themed (iter 5) pero el ornament secundario seguía hardcoded. Ahora el theme switcher cambia absolutamente todo el chrome. Tests: 117 verdes. Downstream compila. Smoke nakui-ui: bootstrap OK. Limitación: nouser-explorer todavía hardcoded — próxima iter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,13 +20,19 @@
|
||||
//! Cuando necesitemos algo serio (selección, posiciones, IME), portamos el
|
||||
//! ejemplo `gpui::examples::input` o adoptamos `gpui-input` cuando exista.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
Context, EventEmitter, FocusHandle, Focusable, IntoElement, KeyDownEvent, Render,
|
||||
SharedString, Window, div, prelude::*, px,
|
||||
SharedString, Task, Window, div, prelude::*, px,
|
||||
};
|
||||
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
/// Período de toggle del caret. 500ms es el estándar de los inputs
|
||||
/// del SO; ni rápido demasiado (distrae) ni lento (parece muerto).
|
||||
const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TextInputEvent {
|
||||
/// El usuario apretó Enter. El payload es el texto actual.
|
||||
@@ -40,6 +46,14 @@ pub struct TextInput {
|
||||
focus_handle: FocusHandle,
|
||||
/// Placeholder visible cuando `text` está vacío.
|
||||
placeholder: SharedString,
|
||||
/// Toggle del caret: alterna cada [`CARET_BLINK_INTERVAL`]
|
||||
/// entre `true` (visible) y `false` (oculto). El render lo
|
||||
/// considera junto con focus para decidir si dibujar `|`.
|
||||
caret_visible: bool,
|
||||
/// Task del loop de blink. Se mantiene en self para que el
|
||||
/// drop del widget cancele el loop (sino seguiría tickeando
|
||||
/// y notificando contra un Entity ya muerto).
|
||||
_blink_task: Task<()>,
|
||||
}
|
||||
|
||||
impl EventEmitter<TextInputEvent> for TextInput {}
|
||||
@@ -53,10 +67,30 @@ impl Focusable for TextInput {
|
||||
impl TextInput {
|
||||
pub fn new(initial: impl Into<String>, cx: &mut Context<Self>) -> Self {
|
||||
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||
// Loop de blink: alterna `caret_visible` y notifica para
|
||||
// re-render. Vive en `_blink_task` (drop = cancel).
|
||||
let blink_task = cx.spawn(async move |this, cx| {
|
||||
let timer = cx.background_executor().clone();
|
||||
loop {
|
||||
timer.timer(CARET_BLINK_INTERVAL).await;
|
||||
let updated = this
|
||||
.update(cx, |me, cx| {
|
||||
me.caret_visible = !me.caret_visible;
|
||||
cx.notify();
|
||||
})
|
||||
.is_ok();
|
||||
if !updated {
|
||||
// Entity drop → salimos del loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
text: initial.into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
placeholder: SharedString::from(""),
|
||||
caret_visible: true,
|
||||
_blink_task: blink_task,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,12 +169,13 @@ impl Render for TextInput {
|
||||
} else {
|
||||
theme.border
|
||||
};
|
||||
// Caret visible cuando: (1) input tiene focus AND (2) el
|
||||
// toggle del blink loop está en `true`. El loop alterna
|
||||
// cada 500ms — feel familiar a los inputs del SO.
|
||||
let show_caret = is_focused && self.caret_visible;
|
||||
let display: SharedString = if is_empty {
|
||||
self.placeholder.clone()
|
||||
} else if is_focused {
|
||||
// Caret al final, sólo cuando el input tiene focus —
|
||||
// así el usuario ve dónde va a aparecer el siguiente
|
||||
// char. Inputs sin focus no muestran caret (es ruido).
|
||||
} else if show_caret {
|
||||
SharedString::from(format!("{}|", self.text))
|
||||
} else {
|
||||
SharedString::from(self.text.clone())
|
||||
|
||||
Reference in New Issue
Block a user