feat(tahuantinsuyu): fase 19 — theme switcher + house tooltips + midpoints + city presets
Fase completa con 4 mejoras independientes que aprovechan toda la
infraestructura previa. La aplicación ahora cubre lecturas profundas
(midpoints uranianos), accesibilidad visual (tooltips de cusps),
personalización (6 themes vía yahweh-widget-theme-switcher) y
usabilidad pragmática (city presets en el form).
## C — Theme switcher en header
- apps/tahuantinsuyu: nueva dep yahweh-widget-theme-switcher.
- shell render(): theme_switcher(cx) en el extremo derecho del header
(con flex_grow del divider del medio). Click cicla entre los 6
presets de yahweh-theme (Nebula, Aurora, Sunset, FlatDark,
SolarizedLight, HighContrast). AstroPalette::for_theme(theme) lee
is_dark, así toda la rueda se re-tinta automáticamente.
## B — Tooltips sobre house cusps
- canvas: HoverInfo deja de ser struct para ser enum con variantes
Body { ... } y HouseCusp { house_number, deg, local_x, local_y }.
Helpers .local() y .key() unifican el acceso.
- on_hover_check: primero hit-test bodies (threshold 14px); si no hubo
match Y el mouse está dentro del anillo de casas
(houses_inner..houses_outer ± 6px), calcula la longitud zodiacal
desde el ángulo de pantalla (inversa de polar_to_screen) y busca el
cusp más cercano (proximidad angular < 2.5°). HoverInfo::HouseCusp.
- Tooltip render: "Cusp Casa N · Signo XX.X°".
## D — MidpointsModule (Uranian-lite)
- engine: PipelineRequest::Midpoints (sin parámetros, default empty).
- bridge: build_midpoints_overlay computa midpoints entre todos los
pares de placements donde involucran Sol o Luna (~10 puntos según
body set). Fórmula: si |a-b|>180, mid=((a+b)/2+180) mod 360, sino
(a+b)/2 mod 360. Emite como Layer { kind: Midpoints, module_id:
"midpoints", ring: 0.62 } con Glyph.symbol="sun/jupiter" y
annotation="Sun/Jupiter".
- modules: midpoints::MidpointsModule con toggle "Activar". Registry
pasa a 7 módulos. Test actualizado.
- shell: build_requests detecta midpoints.enabled, pushea
PipelineRequest::Midpoints (no toma age ni body — es derivado puro).
- canvas: Radii agrega midpoints: r * 0.62 (entre houses_inner y
bodies natales). body_ring("midpoints") y aspect_endpoints retornan
ese radio. paint_wheel agrega un loop para LayerKind::Midpoints
pintando dots pequeños (r=0.012, alpha 0.7 sobre house_cusp color)
— los midpoints no llevan unicode symbol propio (no existe en
Unicode astrológico estándar). El detalle del par viene en hover.
- Hover sobre un midpoint: tooltip muestra "☉/♄ Tauro 14.3° ·
Sun/Jupiter" (display_symbol parsea "a/b" en dos unicodes;
annotation incluye nombres completos eternal).
## A — City presets en el ChartForm
- tree: nueva const CITY_PRESETS con 25 ciudades (Latinoamérica
capitales + 5 europeas + 5 anglosajonas + Tokyo/Sydney/Mumbai/Cairo)
con (name, lat, lon, tz_offset_minutes) sin DST. CityPreset struct.
- tree: TahuantinsuyuTree gana city_picker_open: bool. close_modal
lo resetea. toggle_city_picker + apply_city_preset(preset) helpers.
apply_city_preset lee el Modal activo (CreateChart o EditChart),
llama TextInput::set_text en place/lat/lon/tz del ChartForm,
cierra el picker.
- render_chart_form: title_row ahora tiene "📍 Ciudad rápida ▾"
button a la derecha del title. Click → toggle. Cuando picker_open,
popup absoluto debajo con la lista de presets. Click en preset →
autocompleta + cierra. El usuario sigue pudiendo editar manualmente
cualquier campo después; el preset es solo un punto de partida
rápido para evitar tipear coordenadas a mano.
cargo check verde, 8 tests engine + 1 test modules (7 módulos)
verdes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -147,8 +147,51 @@ pub struct TahuantinsuyuTree {
|
||||
expanded: HashSet<String>,
|
||||
menu: Option<MenuState>,
|
||||
modal: Option<Modal>,
|
||||
/// `true` cuando el dropdown de "ciudad rápida" en el ChartForm
|
||||
/// está abierto. Vive en el tree (no en ChartForm) porque las
|
||||
/// closures de los click handlers necesitan mutarlo via `cx.listener`.
|
||||
city_picker_open: bool,
|
||||
}
|
||||
|
||||
/// Preset de ciudad con datos canónicos para autocompletar lat/lon/tz
|
||||
/// al elegirlo en el form. TZ es la zona estándar **sin DST** — el
|
||||
/// usuario afina si necesita.
|
||||
#[derive(Clone, Copy)]
|
||||
struct CityPreset {
|
||||
name: &'static str,
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
tz_offset_minutes: i32,
|
||||
}
|
||||
|
||||
const CITY_PRESETS: &[CityPreset] = &[
|
||||
CityPreset { name: "Buenos Aires, AR", lat: -34.6037, lon: -58.3816, tz_offset_minutes: -180 },
|
||||
CityPreset { name: "Caracas, VE", lat: 10.4806, lon: -66.9036, tz_offset_minutes: -240 },
|
||||
CityPreset { name: "Bogotá, CO", lat: 4.7110, lon: -74.0721, tz_offset_minutes: -300 },
|
||||
CityPreset { name: "Lima, PE", lat: -12.0464, lon: -77.0428, tz_offset_minutes: -300 },
|
||||
CityPreset { name: "Santiago, CL", lat: -33.4489, lon: -70.6693, tz_offset_minutes: -240 },
|
||||
CityPreset { name: "Quito, EC", lat: -0.1807, lon: -78.4678, tz_offset_minutes: -300 },
|
||||
CityPreset { name: "Montevideo, UY", lat: -34.9011, lon: -56.1645, tz_offset_minutes: -180 },
|
||||
CityPreset { name: "Asunción, PY", lat: -25.2637, lon: -57.5759, tz_offset_minutes: -240 },
|
||||
CityPreset { name: "La Paz, BO", lat: -16.4897, lon: -68.1193, tz_offset_minutes: -240 },
|
||||
CityPreset { name: "Ciudad de México", lat: 19.4326, lon: -99.1332, tz_offset_minutes: -360 },
|
||||
CityPreset { name: "Madrid, ES", lat: 40.4168, lon: -3.7038, tz_offset_minutes: 60 },
|
||||
CityPreset { name: "Barcelona, ES", lat: 41.3851, lon: 2.1734, tz_offset_minutes: 60 },
|
||||
CityPreset { name: "London, UK", lat: 51.5074, lon: -0.1278, tz_offset_minutes: 0 },
|
||||
CityPreset { name: "Paris, FR", lat: 48.8566, lon: 2.3522, tz_offset_minutes: 60 },
|
||||
CityPreset { name: "Berlin, DE", lat: 52.5200, lon: 13.4050, tz_offset_minutes: 60 },
|
||||
CityPreset { name: "Roma, IT", lat: 41.9028, lon: 12.4964, tz_offset_minutes: 60 },
|
||||
CityPreset { name: "New York, US", lat: 40.7128, lon: -74.0060, tz_offset_minutes: -300 },
|
||||
CityPreset { name: "Los Angeles, US", lat: 34.0522, lon: -118.2437, tz_offset_minutes: -480 },
|
||||
CityPreset { name: "Chicago, US", lat: 41.8781, lon: -87.6298, tz_offset_minutes: -360 },
|
||||
CityPreset { name: "São Paulo, BR", lat: -23.5505, lon: -46.6333, tz_offset_minutes: -180 },
|
||||
CityPreset { name: "Rio de Janeiro, BR", lat: -22.9068, lon: -43.1729, tz_offset_minutes: -180 },
|
||||
CityPreset { name: "Tokyo, JP", lat: 35.6762, lon: 139.6503, tz_offset_minutes: 540 },
|
||||
CityPreset { name: "Sydney, AU", lat: -33.8688, lon: 151.2093, tz_offset_minutes: 600 },
|
||||
CityPreset { name: "Mumbai, IN", lat: 19.0760, lon: 72.8777, tz_offset_minutes: 330 },
|
||||
CityPreset { name: "Cairo, EG", lat: 30.0444, lon: 31.2357, tz_offset_minutes: 120 },
|
||||
];
|
||||
|
||||
impl EventEmitter<TreeEvent> for TahuantinsuyuTree {}
|
||||
|
||||
impl TahuantinsuyuTree {
|
||||
@@ -167,6 +210,7 @@ impl TahuantinsuyuTree {
|
||||
expanded: HashSet::new(),
|
||||
menu: None,
|
||||
modal: None,
|
||||
city_picker_open: false,
|
||||
};
|
||||
me.refresh(cx);
|
||||
me
|
||||
@@ -292,10 +336,43 @@ impl TahuantinsuyuTree {
|
||||
|
||||
fn close_modal(&mut self, cx: &mut Context<Self>) {
|
||||
if self.modal.take().is_some() {
|
||||
self.city_picker_open = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_city_picker(&mut self, cx: &mut Context<Self>) {
|
||||
self.city_picker_open = !self.city_picker_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Aplica un city preset al ChartForm activo (CreateChart o
|
||||
/// EditChart). Setea place, lat, lon, tz_offset_min vía
|
||||
/// `TextInput::set_text` y cierra el picker.
|
||||
fn apply_city_preset(&mut self, preset: CityPreset, cx: &mut Context<Self>) {
|
||||
let form = match self.modal.as_mut() {
|
||||
Some(Modal::CreateChart { form, .. }) => form,
|
||||
Some(Modal::EditChart { form, .. }) => form,
|
||||
_ => {
|
||||
self.city_picker_open = false;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let place = form.place.clone();
|
||||
let lat = form.lat.clone();
|
||||
let lon = form.lon.clone();
|
||||
let tz = form.tz_offset_min.clone();
|
||||
place.update(cx, |i, cx| i.set_text(preset.name.to_string(), cx));
|
||||
lat.update(cx, |i, cx| i.set_text(format!("{}", preset.lat), cx));
|
||||
lon.update(cx, |i, cx| i.set_text(format!("{}", preset.lon), cx));
|
||||
tz.update(cx, |i, cx| {
|
||||
i.set_text(preset.tz_offset_minutes.to_string(), cx)
|
||||
});
|
||||
self.city_picker_open = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn open_create_group(
|
||||
&mut self,
|
||||
parent: Option<GroupId>,
|
||||
@@ -1139,6 +1216,78 @@ fn render_chart_form(
|
||||
this.close_modal(cx);
|
||||
}));
|
||||
|
||||
// Header del form: title + botón "Ciudad rápida" con dropdown
|
||||
// que autocompleta place/lat/lon/tz al elegir un preset.
|
||||
let picker_open = cx.entity().read(cx).city_picker_open;
|
||||
let city_btn = div()
|
||||
.id("tts-form-city-btn")
|
||||
.px(px(10.0))
|
||||
.py(px(4.0))
|
||||
.rounded(px(4.0))
|
||||
.bg(theme.bg_button())
|
||||
.hover(|s| s.bg(theme.bg_button_hover()))
|
||||
.border_1()
|
||||
.border_color(if picker_open {
|
||||
theme.accent_strong
|
||||
} else {
|
||||
theme.border
|
||||
})
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child("📍 Ciudad rápida ▾")
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.toggle_city_picker(cx);
|
||||
}));
|
||||
let title_row = div()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(12.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(14.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(title.to_string())),
|
||||
)
|
||||
.child(div().flex_grow())
|
||||
.child(city_btn);
|
||||
let title_row = if picker_open {
|
||||
let mut popup = div()
|
||||
.absolute()
|
||||
.top(px(36.0))
|
||||
.right(px(0.0))
|
||||
.min_w(px(220.0))
|
||||
.max_h(px(320.0))
|
||||
.py(px(4.0))
|
||||
.bg(theme.bg_panel_alt.clone())
|
||||
.border_1()
|
||||
.border_color(theme.border_strong)
|
||||
.rounded(px(6.0))
|
||||
.flex()
|
||||
.flex_col();
|
||||
for preset in CITY_PRESETS.iter().copied() {
|
||||
let row_id: SharedString =
|
||||
SharedString::from(format!("tts-city-{}", preset.name));
|
||||
popup = popup.child(
|
||||
div()
|
||||
.id(gpui::ElementId::from(row_id))
|
||||
.px(px(10.0))
|
||||
.py(px(4.0))
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.hover(|s| s.bg(theme.bg_row_hover))
|
||||
.child(SharedString::from(preset.name.to_string()))
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.apply_city_preset(preset, cx);
|
||||
})),
|
||||
);
|
||||
}
|
||||
title_row.child(popup)
|
||||
} else {
|
||||
title_row
|
||||
};
|
||||
|
||||
let mut body = div()
|
||||
.min_w(px(640.0))
|
||||
.p(px(18.0))
|
||||
@@ -1149,12 +1298,7 @@ fn render_chart_form(
|
||||
.border_1()
|
||||
.border_color(theme.border_strong)
|
||||
.rounded(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(14.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(title.to_string())),
|
||||
)
|
||||
.child(title_row)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
||||
Reference in New Issue
Block a user