From 0ae622550d62cf0f7dda779729f6a55d312c85f1 Mon Sep 17 00:00:00 2001 From: sergio Date: Sun, 17 May 2026 11:45:58 +0000 Subject: [PATCH] =?UTF-8?q?feat(tahuantinsuyu):=20fase=2017=20=E2=80=94=20?= =?UTF-8?q?filtros=20de=20aspectos=20+=20editor=20+=20cleanup=20+=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fase completa con 4 mejoras independientes que reusan toda la infraestructura previa: ## A — Filtros de aspectos en NatalModule NatalModule gana 3 controles nuevos que SÍ recomponen (a diferencia de los show_* que solo togglean visibilidad): - Toggle "Mayores (☌ ☍ △ □ ⚹)" default true - Toggle "Menores (quincunx, semi-…)" default false - Slider "Multiplicador de orbe" range 0.25..2.5 step 0.25 default 1.0 Engine API extendida sin romper la existente: - pub struct NatalOptions { show_majors, show_minors, orb_multiplier } - pub fn compose_with_options(chart, offset, requests, &NatalOptions) - compose() queda como wrapper con NatalOptions::default() - bridge::compose acepta el natal_options, construye OrbTable escalada (build_orb_table multiplier) y filtra aspects antes de pasarlos a build_render_model. Build_render_model dejó de filtrar majors internamente — ahora respeta lo que recibe. Shell wire: - build_natal_options() lee aspect_majors/aspect_minors/orb_multiplier desde module_configs["natal"] con defaults seguros. - on_panel_event para natal: si key empieza con "show_" → canvas visibility (sin recompose); otherwise → update module_configs + persist + render_current. - render_current pasa natal_options a compose_with_options. ## B — Editor de carta natal existente - Store::update_chart(id, label, &birth, &config) — actualiza tres columnas preservando id/contact_id/related/created_at_ms y todo el module_state asociado (la FK CASCADE no se dispara por UPDATE). - Tree: Modal::EditChart { id, form, error } reusa ChartForm que ya manejaba el create. open_edit_chart(id, w, cx) lee la carta con store.get_chart, pre-carga cada TextInput con el valor existente (label, birthplace, año, mes, día, hora, min, tz, lat, lon, alt). submit_modal::EditChart lee form, llama update_chart, preserva el config existente (zodiac/house_system/bodies no se editan acá). Menú contextual del chart agrega "Editar…" entre "Abrir" y "Renombrar". - render_chart_form ahora toma `title: &str` parameter para que el modal muestre "Editar carta natal" vs "Nueva carta natal". El botón cambia "Crear carta" → "Guardar cambios" según el title. ## C — Single source of truth para OUTER_RING_MODULES - engine exporta `pub const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"]` - shell elimina su const local, importa del engine - canvas elimina 4 listas hardcodeadas (paint_wheel outer ring active check + glyphs overlay + aspect_endpoints match) y usa contains() o early-return sobre el slice. Próximo módulo outer-ring solo necesita agregarse al const, no buscar copias. ## D — Labels ASC/MC/DESC/IC en el perímetro Cuatro centered_glyphs en radii.sign_outer * 1.06 (justo afuera del dial zodiacal, dentro del WHEEL_MARGIN) con color angle_highlight y font 10px. El ojo identifica los 4 ángulos inmediatamente sin tener que mapear la línea radial gruesa al ángulo correspondiente. Las posiciones rotan con la rueda (drag del jog-dial los lleva). `cargo check` y `cargo test` verdes. La fase agregó 6 controles visibles al panel del NatalModule (4 view + 2 aspect filter + 1 slider) sin tocar la arquitectura de fases 6-15. Co-Authored-By: Claude Opus 4.7 --- crates/apps/tahuantinsuyu/src/shell.rs | 55 ++++++- .../tahuantinsuyu-canvas/src/lib.rs | 47 ++++-- .../tahuantinsuyu-engine/src/bridge.rs | 40 +++++- .../tahuantinsuyu-engine/src/lib.rs | 47 +++++- .../tahuantinsuyu-modules/src/lib.rs | 22 +++ .../tahuantinsuyu-store/src/lib.rs | 25 ++++ .../tahuantinsuyu-tree/src/lib.rs | 134 +++++++++++++++++- 7 files changed, 335 insertions(+), 35 deletions(-) diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs index 43d6960..a85f42d 100644 --- a/crates/apps/tahuantinsuyu/src/shell.rs +++ b/crates/apps/tahuantinsuyu/src/shell.rs @@ -31,7 +31,9 @@ use gpui::{ use tahuantinsuyu_canvas::{ AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope, }; -use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose}; +use tahuantinsuyu_engine::{ + LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options, +}; use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection}; use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent}; use tahuantinsuyu_store::Store; @@ -287,6 +289,28 @@ impl Shell { siblings.into_iter().find(|c| c.id != current.id) } + /// Deriva las `NatalOptions` activas a partir del `module_configs["natal"]`. + /// Si la entry no existe, devuelve defaults (majors=true, minors=false, + /// multiplier=1.0). + fn build_natal_options(&self) -> NatalOptions { + let cfg = self.module_configs.get("natal"); + let read_bool = |key: &str, default: bool| -> bool { + cfg.and_then(|c| c.get(key)) + .and_then(|v| v.as_bool()) + .unwrap_or(default) + }; + let read_f64 = |key: &str, default: f64| -> f64 { + cfg.and_then(|c| c.get(key)) + .and_then(|v| v.as_f64()) + .unwrap_or(default) + }; + NatalOptions { + show_majors: read_bool("aspect_majors", true), + show_minors: read_bool("aspect_minors", false), + orb_multiplier: read_f64("orb_multiplier", 1.0), + } + } + /// Lee `module_state` desde SQLite para la carta dada y los mergea /// con los defaults ya cargados en `module_configs`. Los valores /// persistidos ganan sobre los defaults. @@ -406,7 +430,13 @@ impl Shell { return; }; let requests = self.build_requests(); - let render = match compose(chart, self.current_offset_minutes, &requests) { + let natal_options = self.build_natal_options(); + let render = match compose_with_options( + chart, + self.current_offset_minutes, + &requests, + &natal_options, + ) { Ok(r) => r, Err(e) => { eprintln!( @@ -475,8 +505,9 @@ impl Shell { } => { let bool_val = value.as_bool().unwrap_or(true); if module_id == "natal" { - // Toggles puramente visuales — solo afectan visibility - // del render actual, sin recomponer. + // Distinguimos: show_* = visibility (no recompose), + // aspect_*/orb_* = filtros de engine (recompose + + // persist). let kind = match key.as_str() { "show_sign_dial" => Some(LayerKind::SignDial), "show_houses" => Some(LayerKind::Houses), @@ -487,6 +518,17 @@ impl Shell { if let Some(k) = kind { self.canvas .update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx)); + } else { + // Filtros: actualizar module_configs + recompose. + let entry = self + .module_configs + .entry("natal".into()) + .or_insert_with(|| serde_json::json!({})); + if let serde_json::Value::Object(map) = entry { + map.insert(key.clone(), value.clone()); + } + self.persist_module("natal"); + self.render_current(cx); } } else { // Cualquier otro módulo: actualizamos su config y @@ -540,9 +582,8 @@ impl Shell { // Helpers de module_configs // ===================================================================== -/// Módulos que pintan en el outer ring del canvas — mutuamente -/// excluyentes a nivel de UI. Al prender uno, los otros se apagan. -const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"]; +// OUTER_RING_MODULES viene de tahuantinsuyu_engine — single source of +// truth. Shell y canvas leen del mismo slice. /// Etiqueta breve para mostrar al elegir una carta en el picker: diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs index 6d1f23c..83341ff 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs @@ -40,7 +40,7 @@ use gpui::{ linear_color_stop, linear_gradient, point, prelude::*, px, }; -use tahuantinsuyu_engine::{Geometry, Layer, LayerKind, RenderModel}; +use tahuantinsuyu_engine::{Geometry, Layer, LayerKind, OUTER_RING_MODULES, RenderModel}; use tahuantinsuyu_model::{ChartId, ContactId, GroupId}; use tahuantinsuyu_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet}; use yahweh_theme::Theme; @@ -584,9 +584,7 @@ fn render_wheel( if visible.get(&LayerKind::Outer).copied().unwrap_or(true) { for layer in &render.layers { if matches!(layer.kind, LayerKind::Outer) - && (layer.module_id == "transit" - || layer.module_id == "synastry" - || layer.module_id == "planetary_return") + && (OUTER_RING_MODULES.contains(&layer.module_id.as_str())) { for g in &layer.glyphs { let (x, y) = polar_to_screen(g.deg, asc, rot_offset, radii.transits); @@ -609,6 +607,29 @@ fn render_wheel( } } + // Labels ASC/MC/DESC/IC en el perímetro. Texto pequeño en el + // margen exterior (radius * 1.05) para que no se monte con los + // glifos de los signos. Color angle_highlight para que el ojo los + // reconozca como los cuatro ángulos cardinales. + let angle_labels = [ + (asc, "ASC"), + (render.midheaven_deg, "MC"), + (render.descendant_deg, "DESC"), + (render.imum_coeli_deg, "IC"), + ]; + let label_r = r_outer * 1.06; + for (deg, label) in angle_labels { + let (x, y) = polar_to_screen(deg, asc, rot_offset, label_r); + wheel = wheel.child(centered_glyph( + cx_center + x, + cy_center + y, + 32.0, + 10.0, + label.into(), + palette.angle_highlight, + )); + } + // --- Header + footer + indicador de tiempo --- let header = div() .flex() @@ -789,12 +810,14 @@ impl Radii { /// Resuelve qué radios corresponden a una capa de aspectos según el /// `module_id`: natal-natal en `aspects`, cross con cada overlay - /// desde `bodies` (extremo natal) al ring del módulo. Synastry y - /// Planetary Return comparten el outer ring de tránsito (los tres - /// son mutuamente excluyentes a nivel de Shell). + /// desde `bodies` (extremo natal) al ring del módulo. Los módulos + /// del outer ring (OUTER_RING_MODULES) comparten el slot de + /// tránsito (son mutuamente excluyentes a nivel de Shell). fn aspect_endpoints(&self, module_id: &str) -> (f32, f32) { + if OUTER_RING_MODULES.contains(&module_id) { + return (self.bodies, self.transits); + } match module_id { - "transit" | "synastry" | "planetary_return" => (self.bodies, self.transits), "progression" => (self.bodies, self.progression), "solar_arc" => (self.bodies, self.solar_arc), _ => (self.aspects, self.aspects), @@ -1012,9 +1035,7 @@ fn paint_wheel( // si alguno de los dos está prendido, pintamos el slot. let outer_active = layers.iter().any(|l| { matches!(l.kind, LayerKind::Outer) - && (l.module_id == "transit" - || l.module_id == "synastry" - || l.module_id == "planetary_return") + && OUTER_RING_MODULES.contains(&l.module_id.as_str()) }); if outer_active && show(LayerKind::Outer) { stroke_circle( @@ -1037,9 +1058,7 @@ fn paint_wheel( let dot_r = (radii.sign_outer * 0.017).max(2.0); for layer in layers { if matches!(layer.kind, LayerKind::Outer) - && (layer.module_id == "transit" - || layer.module_id == "synastry" - || layer.module_id == "planetary_return") + && (OUTER_RING_MODULES.contains(&layer.module_id.as_str())) { for g in &layer.glyphs { let color = with_alpha(planet_color(palette, &g.symbol), 0.85); diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs index 29870e5..ca29fe2 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs @@ -244,10 +244,20 @@ pub fn compose( chart: &Chart, offset_minutes: i64, requests: &[crate::PipelineRequest], + natal_options: &crate::NatalOptions, ) -> Result { let t0 = Instant::now(); let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?; - let aspects = find_aspects(&natal, &OrbTable::modern_western()); + let orb_table = build_orb_table(natal_options.orb_multiplier); + let all_aspects = find_aspects(&natal, &orb_table); + let aspects: Vec = all_aspects + .into_iter() + .filter(|a| { + let is_major = EAspectKind::MAJORS.contains(&a.kind); + (is_major && natal_options.show_majors) + || (!is_major && natal_options.show_minors) + }) + .collect(); let mut render = build_render_model(chart, &natal, &aspects, t0); for req in requests { @@ -776,13 +786,10 @@ fn build_render_model( }; // ─── Capa 3: Aspects ────────────────────────────────────────────── + // Los aspects ya vienen filtrados por NatalOptions (majors / minors) + // desde compose(). Acá solo mapeamos a LineSeg. let mut aspect_lines: Vec = Vec::with_capacity(aspects.len()); for a in aspects { - // Solo los aspectos mayores se pintan en este pase — los menores - // saturan visualmente. Fase 4 pondrá un toggle para mostrarlos. - if !EAspectKind::MAJORS.contains(&a.kind) { - continue; - } let pa = natal.placement(a.a); let pb = natal.placement(a.b); if let (Some(pa), Some(pb)) = (pa, pb) { @@ -834,6 +841,27 @@ fn build_render_model( } } +/// Construye una `OrbTable` con los orbes default de `modern_western` +/// escalados por `multiplier`. Necesario porque eternal expone +/// `set_orb` pero no permite iterar los base orbs internos. +fn build_orb_table(multiplier: f64) -> OrbTable { + let mut t = OrbTable::modern_western(); + let m = multiplier.max(0.0); + t.set_orb(EAspectKind::Conjunction, 8.0 * m); + t.set_orb(EAspectKind::Opposition, 8.0 * m); + t.set_orb(EAspectKind::Trine, 7.0 * m); + t.set_orb(EAspectKind::Square, 7.0 * m); + t.set_orb(EAspectKind::Sextile, 5.0 * m); + t.set_orb(EAspectKind::Quincunx, 2.5 * m); + t.set_orb(EAspectKind::SemiSextile, 2.0 * m); + t.set_orb(EAspectKind::SemiSquare, 2.0 * m); + t.set_orb(EAspectKind::Sesquiquadrate, 2.0 * m); + t.set_orb(EAspectKind::Quintile, 1.5 * m); + t.set_orb(EAspectKind::BiQuintile, 1.5 * m); + t.set_orb(EAspectKind::Septile, 1.5 * m); + t +} + fn push_overlay_meta(render: &mut RenderModel, module_id: &str, label: String) { render.overlays.push(OverlayMeta { module_id: module_id.to_string(), diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs index 533cfca..e47fdc0 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs @@ -178,6 +178,12 @@ pub enum EngineError { /// `tahuantinsuyu-modules` por id string. Esto deja la engine como /// dueña única del cómputo (no depende del trait Module — los módulos /// son sólo metadata + UI controls). +/// Módulos overlay que pintan en el mismo slot (outer ring del wheel) +/// y por lo tanto son **mutuamente excluyentes** a nivel de UI: al +/// prender uno, el shell debe apagar los otros. Single source of truth +/// — el shell y el canvas leen de acá en vez de hardcodear listas. +pub const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"]; + #[derive(Debug, Clone)] pub enum PipelineRequest { /// `module_id = "transit"` — anillo externo con planetas al @@ -220,21 +226,54 @@ pub enum PipelineRequest { }, } +/// Opciones que afectan la pasada natal (qué aspectos pintar, qué +/// multiplicador de orbe usar). Es independiente de los overlays. +#[derive(Debug, Clone)] +pub struct NatalOptions { + /// Incluir aspectos mayores (conj/opp/trine/square/sextile). + pub show_majors: bool, + /// Incluir aspectos menores (quincunx/semi-sextile/etc). + pub show_minors: bool, + /// Multiplicador uniforme sobre los orbes default. `1.0` = orbes + /// modern_western; `0.5` = tight; `2.0` = wide. + pub orb_multiplier: f64, +} + +impl Default for NatalOptions { + fn default() -> Self { + Self { + show_majors: true, + show_minors: false, + orb_multiplier: 1.0, + } + } +} + /// Composición canónica: carta natal + todos los overlays pedidos. -/// Es la única función que el Shell necesita llamar — `compute_at_offset` -/// y `compute_with_transits_at_now` quedan como atajos retrocompatibles. +/// Equivalente a `compose_with_options` con `NatalOptions::default()`. pub fn compose( chart: &Chart, offset_minutes: i64, requests: &[PipelineRequest], +) -> Result { + compose_with_options(chart, offset_minutes, requests, &NatalOptions::default()) +} + +/// Variante que permite controlar qué aspectos natales se computan y +/// con qué multiplicador de orbe. +pub fn compose_with_options( + chart: &Chart, + offset_minutes: i64, + requests: &[PipelineRequest], + natal_options: &NatalOptions, ) -> Result { #[cfg(feature = "eternal-bridge")] { - bridge::compose(chart, offset_minutes, requests) + bridge::compose(chart, offset_minutes, requests, natal_options) } #[cfg(not(feature = "eternal-bridge"))] { - let _ = (offset_minutes, requests); + let _ = (offset_minutes, requests, natal_options); Ok(compute_mock(chart)) } } diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs index 4e67357..9a8320f 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs @@ -217,6 +217,28 @@ pub mod natal { default: true, hotkey: Some("P".into()), }, + // Filtros de aspectos: cambian QUÉ se computa, no QUÉ + // se pinta del render. Recompose al togglear. + Control::Toggle { + key: "aspect_majors".into(), + label: "Mayores (☌ ☍ △ □ ⚹)".into(), + default: true, + hotkey: None, + }, + Control::Toggle { + key: "aspect_minors".into(), + label: "Menores (quincunx, semi-…)".into(), + default: false, + hotkey: None, + }, + Control::Slider { + key: "orb_multiplier".into(), + label: "Multiplicador de orbe".into(), + min: 0.25, + max: 2.5, + step: 0.25, + default: 1.0, + }, Control::Slider { key: "harmonic".into(), label: "Armónico".into(), diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs index 557c638..6742cd9 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs @@ -343,6 +343,31 @@ impl Store { Ok(()) } + /// Reemplaza label + birth_data + config de una carta existente, + /// preservando id / contact_id / related_chart_id / created_at_ms y + /// el `module_state` asociado (no se borra). Usado por el editor de + /// rectificación natal. + pub fn update_chart( + &self, + id: ChartId, + label: &str, + birth: &StoredBirthData, + config: &StoredChartConfig, + ) -> StoreResult<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "UPDATE charts SET label = ?2, birth_data_json = ?3, config_json = ?4 \ + WHERE id = ?1", + params![ + id.to_string(), + label, + serde_json::to_string(birth)?, + serde_json::to_string(config)?, + ], + )?; + Ok(()) + } + // ----------------------------------------------------------------- // Module state // ----------------------------------------------------------------- diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs index 410436e..d22ccea 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs @@ -112,6 +112,15 @@ enum Modal { form: ChartForm, error: Option, }, + /// Editar una carta existente — reusa `ChartForm` pre-cargada. + /// El submit llama `store.update_chart(id, ...)` preservando + /// `chart.contact_id`, `related_chart_id`, `module_state` y el + /// historial. + EditChart { + id: ChartId, + form: ChartForm, + error: Option, + }, } struct ChartForm { @@ -309,6 +318,58 @@ impl TahuantinsuyuTree { self.close_menu(cx); } + fn open_edit_chart( + &mut self, + id: ChartId, + window: &mut Window, + cx: &mut Context, + ) { + // Cargar la carta existente; si no se puede, fallamos en silencio. + let chart = match self.store.get_chart(id) { + Ok(c) => c, + Err(e) => { + eprintln!("[tree] open_edit_chart {}: {}", id, e); + return; + } + }; + let bd = &chart.birth_data; + let form = ChartForm { + name: self.make_input("Etiqueta de la carta", &chart.label, window, cx), + place: self.make_input( + "Lugar (ciudad, país)", + bd.birthplace_label.as_deref().unwrap_or(""), + window, + cx, + ), + year: self.make_input("Año", &bd.year.to_string(), window, cx), + month: self.make_input("Mes", &bd.month.to_string(), window, cx), + day: self.make_input("Día", &bd.day.to_string(), window, cx), + hour: self.make_input("Hora (0-23)", &bd.hour.to_string(), window, cx), + minute: self.make_input("Minuto", &bd.minute.to_string(), window, cx), + tz_offset_min: self.make_input( + "TZ offset (min)", + &bd.tz_offset_minutes.to_string(), + window, + cx, + ), + lat: self.make_input("Latitud (°)", &format!("{}", bd.latitude_deg), window, cx), + lon: self.make_input( + "Longitud (°)", + &format!("{}", bd.longitude_deg), + window, + cx, + ), + alt: self.make_input("Altitud (m)", &format!("{}", bd.altitude_m), window, cx), + }; + form.name.update(cx, |i, _| i.request_focus(window)); + self.modal = Some(Modal::EditChart { + id, + form, + error: None, + }); + self.close_menu(cx); + } + fn open_create_chart( &mut self, contact: ContactId, @@ -521,6 +582,56 @@ impl TahuantinsuyuTree { } } } + Modal::EditChart { + id, + form, + error: _, + } => { + let _ = value; + // Para preservar el ChartConfig original (zodiac, house + // system, bodies, etc.) leemos la carta actual y solo + // sobrescribimos label + birth_data. El editor no toca + // config — eso se haría en un futuro panel de "Config + // de carta". + let existing = match self.store.get_chart(id) { + Ok(c) => c, + Err(e) => { + self.modal = Some(Modal::EditChart { + id, + form, + error: Some(SharedString::from(format!("Store: {}", e))), + }); + cx.notify(); + return; + } + }; + match build_chart_from_form(&form, cx) { + Ok((birth, label)) => { + match self.store.update_chart(id, &label, &birth, &existing.config) { + Ok(_) => { + drop(form); + self.after_mutation(cx); + } + Err(e) => { + self.modal = Some(Modal::EditChart { + id, + form, + error: Some(SharedString::from(format!("Store: {}", e))), + }); + cx.notify(); + } + } + } + Err(msg) => { + self.modal = Some(Modal::EditChart { + id, + form, + error: Some(SharedString::from(msg)), + }); + cx.notify(); + } + } + } } } @@ -829,6 +940,11 @@ impl TahuantinsuyuTree { this.close_menu(cx); }), )); + items = items.child(menu_item("tts-menu-edit-h", "Editar…", theme).on_click( + cx.listener(move |this, _: &ClickEvent, w, cx| { + this.open_edit_chart(id, w, cx); + }), + )); items = items.child(separator(theme)); let t = menu.target.clone(); items = items.child(menu_item("tts-menu-rename-h", "Renombrar…", theme).on_click( @@ -869,7 +985,12 @@ impl TahuantinsuyuTree { input.clone(), "Enter = crear — Escape = cancelar", ), - Modal::CreateChart { form, error, .. } => render_chart_form(theme, form, error.clone(), cx), + Modal::CreateChart { form, error, .. } => { + render_chart_form(theme, "Nueva carta natal", form, error.clone(), cx) + } + Modal::EditChart { form, error, .. } => { + render_chart_form(theme, "Editar carta natal", form, error.clone(), cx) + } }; div() @@ -945,6 +1066,7 @@ fn modal_box( fn render_chart_form( theme: &Theme, + title: &str, form: &ChartForm, error: Option, cx: &mut Context, @@ -991,11 +1113,15 @@ fn render_chart_form( .hover(|s| s.bg(theme.bg_button_hover())) .text_size(px(12.0)) .text_color(theme.fg_text) - .child("Crear carta") + .child(SharedString::from(if title.starts_with("Editar") { + "Guardar cambios" + } else { + "Crear carta" + })) .on_click(cx.listener(|this, _: &ClickEvent, _, cx| { // Disparamos un submit "vacío" — el handler de submit // re-lee todos los campos del form. El value que pasamos - // se ignora dentro del branch CreateChart. + // se ignora dentro del branch CreateChart/EditChart. this.submit_modal(String::new(), cx); })); @@ -1027,7 +1153,7 @@ fn render_chart_form( div() .text_size(px(14.0)) .text_color(theme.fg_text) - .child("Nueva carta natal"), + .child(SharedString::from(title.to_string())), ) .child( div()