feat(tahuantinsuyu): scaffolding del estudio astrológico (10 crates + ventana 3-panes)
Módulo nuevo `modules/tahuantinsuyu/` con 9 crates reusables + app `apps/tahuantinsuyu` ejecutable que abre la ventana del explorador y coordina los widgets: - tahuantinsuyu-card: Card Brahman + spawn_sidecar (flows chart-request/chart-result). - tahuantinsuyu-model: tipos agnósticos (Group/Contact/Chart, StoredBirthData, StoredChartConfig, ChartKind, TreeSelection). - tahuantinsuyu-store: persistencia SQLite (rusqlite) con migración v1, CRUD por entidad y descenso recursivo `charts_under_group`. - tahuantinsuyu-engine: bridge agnóstico al canvas vía `RenderModel` (Layer/Glyph/Geometry). Feature `eternal-bridge` (off por default) reservada para enchufar eternal-astrology desde ~/eternal. - tahuantinsuyu-modules: registry de módulos pluggables (Module trait + Control schema) con `NatalModule` placeholder. - tahuantinsuyu-theme: AstroPalette (elementos / modos / planetas / aspectos) con variantes dark + light sobre yahweh-theme. - tahuantinsuyu-canvas: widget GPUI con CanvasState (Empty / Wheel / Thumbnails). Render placeholder hasta cablear la rueda real. - tahuantinsuyu-tree: explorador izquierdo sobre yahweh-widget-tree, prefijos g:/c:/h: para Group/Contact/Chart. - tahuantinsuyu-panel: control panel inferior que lee Controls de los módulos del registry y los pinta. - apps/tahuantinsuyu: binario `tahuantinsuyu` (launch_app-style) con Shell coordinador (tree↔canvas↔panel), DB en $XDG_DATA_HOME. Workspace Cargo.toml actualizado con los 10 miembros. `cargo check` verde, tests unitarios verdes (model/store/engine/modules/theme/card). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "tahuantinsuyu-panel"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
description = "Tahuantinsuyu — panel de control inferior. Toggles, sliders y selectores por módulo de astrología."
|
||||
|
||||
[dependencies]
|
||||
tahuantinsuyu-model = { path = "../tahuantinsuyu-model" }
|
||||
tahuantinsuyu-modules = { path = "../tahuantinsuyu-modules" }
|
||||
tahuantinsuyu-theme = { path = "../tahuantinsuyu-theme" }
|
||||
yahweh-theme = { workspace = true }
|
||||
gpui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -0,0 +1,255 @@
|
||||
//! `tahuantinsuyu-panel` — control panel inferior de la app.
|
||||
//!
|
||||
//! Lee los módulos disponibles para la carta activa (vía
|
||||
//! [`tahuantinsuyu_modules::Registry::for_kind`]) y pinta sus
|
||||
//! [`Control`]s como toggles / sliders / selects. Cada cambio emite
|
||||
//! [`PanelEvent`] que la app traduce a mutaciones de `ModuleState` en
|
||||
//! la store y a `toggle_module` sobre el canvas.
|
||||
//!
|
||||
//! Fase 1: render placeholder con el listado de módulos disponibles y
|
||||
//! sus controles, sin handlers cableados todavía (la interacción real
|
||||
//! viene con la fase 4).
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use gpui::{
|
||||
Context, EventEmitter, IntoElement, Render, SharedString, Window, div, prelude::*, px,
|
||||
};
|
||||
|
||||
use tahuantinsuyu_model::ChartKind;
|
||||
use tahuantinsuyu_modules::{Control, Registry};
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
// =====================================================================
|
||||
// Eventos
|
||||
// =====================================================================
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PanelEvent {
|
||||
/// Toggle on/off de un módulo entero.
|
||||
ModuleToggled { module_id: String, enabled: bool },
|
||||
/// Cambio de un control puntual.
|
||||
ControlChanged {
|
||||
module_id: String,
|
||||
key: String,
|
||||
value: serde_json::Value,
|
||||
},
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Widget
|
||||
// =====================================================================
|
||||
|
||||
pub struct ControlPanel {
|
||||
/// Módulo activo a mostrar. `None` ⇒ no hay carta seleccionada,
|
||||
/// pintamos un placeholder.
|
||||
active_kind: Option<ChartKind>,
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for ControlPanel {}
|
||||
|
||||
impl ControlPanel {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||
Self { active_kind: None }
|
||||
}
|
||||
|
||||
pub fn set_active_kind(&mut self, kind: Option<ChartKind>, cx: &mut Context<Self>) {
|
||||
self.active_kind = kind;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ControlPanel {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
let registry = Registry::with_builtins();
|
||||
|
||||
let header = div()
|
||||
.h(px(28.0))
|
||||
.px(px(12.0))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.border_b_1()
|
||||
.border_color(theme.border)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child("Panel de control"),
|
||||
)
|
||||
.child(div().ml_auto().text_size(px(10.0)).text_color(theme.fg_disabled).child(
|
||||
match self.active_kind {
|
||||
Some(k) => SharedString::from(format!("{:?}", k)),
|
||||
None => SharedString::from("sin carta activa"),
|
||||
},
|
||||
));
|
||||
|
||||
let mut body = div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.flex_wrap()
|
||||
.gap(px(16.0))
|
||||
.px(px(12.0))
|
||||
.py(px(8.0));
|
||||
|
||||
if let Some(kind) = self.active_kind {
|
||||
for m in registry.for_kind(kind) {
|
||||
body = body.child(render_module(&theme, m));
|
||||
}
|
||||
} else {
|
||||
body = body.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_disabled)
|
||||
.child("Seleccioná una carta para ver sus controles."),
|
||||
);
|
||||
}
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.bg_panel.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(header)
|
||||
.child(body)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_module(theme: &Theme, m: &dyn tahuantinsuyu_modules::Module) -> gpui::Div {
|
||||
let header = div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(6.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(m.label())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(m.description())),
|
||||
);
|
||||
|
||||
let mut controls = div().flex().flex_col().gap(px(4.0));
|
||||
for c in m.controls() {
|
||||
controls = controls.child(render_control(theme, &c));
|
||||
}
|
||||
|
||||
div()
|
||||
.min_w(px(220.0))
|
||||
.p(px(8.0))
|
||||
.rounded(px(6.0))
|
||||
.bg(theme.bg_panel_alt.clone())
|
||||
.border_1()
|
||||
.border_color(theme.border)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(6.0))
|
||||
.child(header)
|
||||
.child(controls)
|
||||
}
|
||||
|
||||
fn render_control(theme: &Theme, c: &Control) -> gpui::Div {
|
||||
match c {
|
||||
Control::Toggle { label, default, hotkey, .. } => {
|
||||
let dot_color = if *default {
|
||||
theme.accent
|
||||
} else {
|
||||
theme.fg_disabled
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.size(px(8.0))
|
||||
.rounded(px(4.0))
|
||||
.bg(dot_color),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(label.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.ml_auto()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(
|
||||
hotkey.clone().map(|h| format!("[{}]", h)).unwrap_or_default(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
Control::Slider {
|
||||
label, min, max, default, ..
|
||||
} => div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(label.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.ml_auto()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(format!(
|
||||
"{} ({}…{})",
|
||||
default, min, max
|
||||
))),
|
||||
),
|
||||
Control::Select { label, default, .. } => div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(label.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.ml_auto()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(default.clone())),
|
||||
),
|
||||
Control::TextInput { label, default, .. } => div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(label.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.ml_auto()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(default.clone())),
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user