feat(tahuantinsuyu): "Cielo ahora" + "General" como rows fijos al top del tree
Dos entradas siempre presentes en la cima del árbol:
1. **⏱ Cielo ahora** (leaf): selecciona una carta efímera del
instante actual en Greenwich (UTC, lat 51.4769°, lon 0°,
alt 47 m). NO se persiste en la store — `build_present_sky_chart`
la construye al vuelo con `Chart { id: Default::default(), ... }`
y birth_data tomado de `SystemTime::now()` via `unix_to_civil_utc`
(algoritmo Howard Hinnant, exacto y proleptic-Gregoriano).
La carta queda **seleccionada por default** al boot — el usuario
abre la app y ya está viendo el firmamento actual, incluso si
no tiene contactos cargados.
2. **◇ General** (branch): contenedor virtual para los contactos
sin grupo asignado (parent=None). Antes esos contactos
aparecían sueltos al nivel raíz; ahora viven dentro de
"General" y se ofrece como destino claro para "Nuevo
contacto" desde su menú. Click sobre General muestra
thumbnails de TODAS las cartas de esos contactos en el canvas.
Soporte en `TreeSelection`: dos variantes nuevas `PresentSky` y
`GeneralRoot`. `parse_row` reconoce los IDs sentinela `sky:now`
y `general`. El shell maneja ambos casos en `apply_selection`:
- PresentSky → set `current_chart` + render
- GeneralRoot → grilla de thumbnails
`MenuTarget::from_selection` mapea PresentSky/GeneralRoot →
MenuTarget::Root (mismo menú "Nuevo grupo / Nuevo contacto").
`unix_to_civil_utc` con 4 tests cubre: epoch (1970-01-01),
2024-02-29 (año bisiesto), pre-epoch (-1 → 1969-12-31), y
year 2000.
Total 10 tests verdes (6 anteriores + 4 nuevos del calendario).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,9 @@ use yahweh_widget_tree::{RowId, RowKind, TreeEvent as InnerTreeEvent, TreeRow, T
|
||||
const PREFIX_GROUP: &str = "g:";
|
||||
const PREFIX_CONTACT: &str = "c:";
|
||||
const PREFIX_CHART: &str = "h:";
|
||||
/// IDs sentinela para los dos rows virtuales fijos al top del tree.
|
||||
const ROW_SKY_NOW: &str = "sky:now";
|
||||
const ROW_GENERAL: &str = "general";
|
||||
|
||||
// =====================================================================
|
||||
// Eventos públicos
|
||||
@@ -74,6 +77,12 @@ impl MenuTarget {
|
||||
TreeSelection::Group(id) => MenuTarget::Group(*id),
|
||||
TreeSelection::Contact(id) => MenuTarget::Contact(*id),
|
||||
TreeSelection::Chart(id) => MenuTarget::Chart(*id),
|
||||
// "General" comparte menu con Root — el target lógico es
|
||||
// crear contactos sin grupo padre.
|
||||
TreeSelection::GeneralRoot => MenuTarget::Root,
|
||||
// "Cielo ahora" no admite operaciones de menu — es una
|
||||
// carta efímera del momento, no se edita ni borra.
|
||||
TreeSelection::PresentSky => MenuTarget::Root,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,8 +391,36 @@ impl TahuantinsuyuTree {
|
||||
|
||||
pub fn refresh(&mut self, cx: &mut Context<'_, Self>) {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
// 1) Cielo ahora — siempre al top, leaf, ícono distintivo.
|
||||
// No participa de search filter (es atemporal).
|
||||
rows.push(TreeRow {
|
||||
id: RowId::new(ROW_SKY_NOW.to_string()),
|
||||
label: "Cielo ahora".to_string(),
|
||||
depth: 0,
|
||||
kind: RowKind::Leaf,
|
||||
expanded: false,
|
||||
icon: Some("⏱".into()),
|
||||
});
|
||||
|
||||
// 2) General — branch fijo. Contiene los contactos sin grupo
|
||||
// asignado (parent=None). Aparece siempre, sin filtro.
|
||||
let general_expanded = self.expanded.contains(ROW_GENERAL);
|
||||
rows.push(TreeRow {
|
||||
id: RowId::new(ROW_GENERAL.to_string()),
|
||||
label: "General".to_string(),
|
||||
depth: 0,
|
||||
kind: RowKind::Branch,
|
||||
expanded: general_expanded,
|
||||
icon: Some("◇".into()),
|
||||
});
|
||||
if general_expanded {
|
||||
self.append_contacts(None, 1, &mut rows);
|
||||
}
|
||||
|
||||
// 3) Resto: groups top-level con sus contenidos.
|
||||
self.append_groups(None, 0, &mut rows);
|
||||
self.append_contacts(None, 0, &mut rows);
|
||||
|
||||
self.inner.update(cx, |t, cx| t.set_rows(rows, cx));
|
||||
}
|
||||
|
||||
@@ -1189,6 +1226,12 @@ fn find_contact_in_groups(
|
||||
|
||||
fn parse_row(id: &RowId) -> Option<TreeSelection> {
|
||||
let s = id.as_str();
|
||||
if s == ROW_SKY_NOW {
|
||||
return Some(TreeSelection::PresentSky);
|
||||
}
|
||||
if s == ROW_GENERAL {
|
||||
return Some(TreeSelection::GeneralRoot);
|
||||
}
|
||||
if let Some(rest) = s.strip_prefix(PREFIX_GROUP) {
|
||||
return rest.parse().ok().map(TreeSelection::Group);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user