feat(tahuantinsuyu): fase 8 — slider interactivo + slider de edad en progression
Los `Control::Slider` del panel ya no son display-only — son arrastrables con el mismo patrón del splitter (canvas absoluto sobre el track + window mouse handlers en cada frame). El `ProgressionModule` ahora expone un slider de `target_age_years` (0..120) que el shell inicializa con la edad actual del sujeto al cargar la carta. - panel: SliderDrag struct + slider_state HashMap + slider_drag Option + métodos start/continue/end_slider_drag + apply_slider_position que calcula fraction desde la posición del mouse relativa al track y emite ControlChanged con el valor float. set_slider(module, key, val) para sincronización externa. set_active_kind ahora inicializa también los sliders desde sus defaults. render_slider pinta track + portion filled + thumb circular + canvas overlay con handlers de drag. Los Slider tienen un valor visible "X.X (min...max)" en el header. - modules: ProgressionModule agrega Control::Slider target_age_years con range 0..120, step 0.25, default 30 (placeholder — el shell lo reescribe con la edad real al cargar la carta). - shell: apply_selection(Chart) ahora calcula current_age, lo inserta en module_configs["progression"]["target_age_years"], y empuja al panel via set_slider. build_requests ya leía target_age_years desde el map (de fase 7), así que ahora el slider lo controla. Mecánica: si activás "Progresión secundaria", el slider arranca en la edad actual del sujeto. Arrastralo a la izquierda y la rueda recompone la carta progresada para esa edad simbólica — vas viendo cómo el sujeto "evoluciona" o "involuciona" a través de su línea temporal interna, con los planetas progresados moviéndose por el anillo interno y los cross aspects con la natal reorganizándose en tiempo real. Same pattern aplica de aquí en más para cualquier slider futuro (harmonic en NatalModule, target_year en SolarArc, orb_multiplier, …). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -115,11 +115,24 @@ impl Shell {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let age = current_age_years(&chart.birth_data);
|
||||||
self.current_chart = Some(chart.clone());
|
self.current_chart = Some(chart.clone());
|
||||||
self.current_offset_minutes = 0;
|
self.current_offset_minutes = 0;
|
||||||
|
// Inicializar la edad objetivo del módulo de progresión
|
||||||
|
// con la edad actual del sujeto, así el slider arranca
|
||||||
|
// "donde corresponde" si el usuario lo activa.
|
||||||
|
let prog_entry = self
|
||||||
|
.module_configs
|
||||||
|
.entry("progression".into())
|
||||||
|
.or_insert_with(|| serde_json::json!({}));
|
||||||
|
if let serde_json::Value::Object(map) = prog_entry {
|
||||||
|
map.insert("target_age_years".into(), serde_json::json!(age));
|
||||||
|
}
|
||||||
self.render_current(cx);
|
self.render_current(cx);
|
||||||
self.panel
|
self.panel.update(cx, |p, cx| {
|
||||||
.update(cx, |p, cx| p.set_active_kind(Some(chart.kind), cx));
|
p.set_active_kind(Some(chart.kind), cx);
|
||||||
|
p.set_slider("progression", "target_age_years", age, cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
TreeSelection::Contact(id) => {
|
TreeSelection::Contact(id) => {
|
||||||
self.current_chart = None;
|
self.current_chart = None;
|
||||||
|
|||||||
@@ -305,15 +305,26 @@ pub mod progression {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn controls(&self) -> Vec<Control> {
|
fn controls(&self) -> Vec<Control> {
|
||||||
vec![Control::Toggle {
|
vec![
|
||||||
|
Control::Toggle {
|
||||||
key: "enabled".into(),
|
key: "enabled".into(),
|
||||||
label: "Activar".into(),
|
label: "Activar".into(),
|
||||||
default: false,
|
default: false,
|
||||||
// Sin hotkey por ahora — el toggle vive en el panel.
|
|
||||||
// Fase 8 puede agregar [G] vía un canal genérico de
|
|
||||||
// ModuleToggleRequested.
|
|
||||||
hotkey: None,
|
hotkey: None,
|
||||||
}]
|
},
|
||||||
|
// El default (30.0) es un placeholder — el shell empuja
|
||||||
|
// la edad actual del sujeto al cargar una carta vía
|
||||||
|
// panel.set_slider("progression", "target_age_years",
|
||||||
|
// current_age).
|
||||||
|
Control::Slider {
|
||||||
|
key: "target_age_years".into(),
|
||||||
|
label: "Edad objetivo (años)".into(),
|
||||||
|
min: 0.0,
|
||||||
|
max: 120.0,
|
||||||
|
step: 0.25,
|
||||||
|
default: 30.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
|||||||
@@ -4,15 +4,19 @@
|
|||||||
//! [`tahuantinsuyu_modules::Registry::for_kind`]) y pinta sus
|
//! [`tahuantinsuyu_modules::Registry::for_kind`]) y pinta sus
|
||||||
//! [`Control`]s como toggles / sliders / selects. Cada cambio emite
|
//! [`Control`]s como toggles / sliders / selects. Cada cambio emite
|
||||||
//! [`PanelEvent`] que la app traduce a mutaciones de visibilidad sobre
|
//! [`PanelEvent`] que la app traduce a mutaciones de visibilidad sobre
|
||||||
//! el canvas (fase 4) y eventualmente a `ModuleState` en la store.
|
//! el canvas y al `module_configs` del shell.
|
||||||
//!
|
//!
|
||||||
//! ## Estado interno
|
//! ## Estado interno
|
||||||
//!
|
//!
|
||||||
//! El panel mantiene un cache `toggle_state` con los valores actuales
|
//! - `toggle_state: HashMap<(module_id, key), bool>` — valor actual de
|
||||||
//! de los toggles por (module_id, key). Inicializa desde los defaults
|
//! cada toggle. Se inicializa lazy desde los defaults del módulo al
|
||||||
//! declarados por el módulo y se actualiza con cada click. Los sliders
|
//! cambiar de `ChartKind`.
|
||||||
//! / selects todavía no son interactivos en fase 4 — quedan como
|
//! - `slider_state: HashMap<(module_id, key), f64>` — valor actual de
|
||||||
//! display de "valor default".
|
//! cada slider. El shell puede sobreescribirlo via [`Self::set_slider`]
|
||||||
|
//! cuando cambia la carta activa (ej. inicializar `target_age_years`
|
||||||
|
//! con la edad actual del sujeto).
|
||||||
|
//! - `slider_drag: Option<SliderDrag>` — slider que está bajo drag
|
||||||
|
//! activo. Mutuamente excluyente: solo se arrastra un slider a la vez.
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(rust_2018_idioms)]
|
#![warn(rust_2018_idioms)]
|
||||||
@@ -20,8 +24,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
ClickEvent, Context, EventEmitter, IntoElement, ParentElement, Render, SharedString, Styled,
|
Bounds, ClickEvent, Context, EventEmitter, IntoElement, MouseButton, MouseDownEvent,
|
||||||
Window, div, prelude::*, px,
|
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, SharedString, Styled,
|
||||||
|
Window, canvas, div, prelude::*, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tahuantinsuyu_model::ChartKind;
|
use tahuantinsuyu_model::ChartKind;
|
||||||
@@ -42,15 +47,27 @@ pub enum PanelEvent {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Estado interno
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct SliderDrag {
|
||||||
|
module_id: String,
|
||||||
|
key: String,
|
||||||
|
min: f64,
|
||||||
|
max: f64,
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Widget
|
// Widget
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
pub struct ControlPanel {
|
pub struct ControlPanel {
|
||||||
active_kind: Option<ChartKind>,
|
active_kind: Option<ChartKind>,
|
||||||
/// Cache de toggles por (module_id, key). Se popula lazy desde los
|
|
||||||
/// defaults la primera vez que se renderea un kind.
|
|
||||||
toggle_state: HashMap<(String, String), bool>,
|
toggle_state: HashMap<(String, String), bool>,
|
||||||
|
slider_state: HashMap<(String, String), f64>,
|
||||||
|
slider_drag: Option<SliderDrag>,
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,21 +79,30 @@ impl ControlPanel {
|
|||||||
Self {
|
Self {
|
||||||
active_kind: None,
|
active_kind: None,
|
||||||
toggle_state: HashMap::new(),
|
toggle_state: HashMap::new(),
|
||||||
|
slider_state: HashMap::new(),
|
||||||
|
slider_drag: None,
|
||||||
registry: Registry::with_builtins(),
|
registry: Registry::with_builtins(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_active_kind(&mut self, kind: Option<ChartKind>, cx: &mut Context<Self>) {
|
pub fn set_active_kind(&mut self, kind: Option<ChartKind>, cx: &mut Context<Self>) {
|
||||||
// Si cambia el kind, inicializamos defaults para sus módulos.
|
|
||||||
if self.active_kind != kind {
|
if self.active_kind != kind {
|
||||||
if let Some(k) = kind {
|
if let Some(k) = kind {
|
||||||
for m in self.registry.for_kind(k) {
|
for m in self.registry.for_kind(k) {
|
||||||
for c in m.controls() {
|
for c in m.controls() {
|
||||||
if let Control::Toggle { key, default, .. } = c {
|
match c {
|
||||||
|
Control::Toggle { key, default, .. } => {
|
||||||
self.toggle_state
|
self.toggle_state
|
||||||
.entry((m.id().to_string(), key))
|
.entry((m.id().to_string(), key))
|
||||||
.or_insert(default);
|
.or_insert(default);
|
||||||
}
|
}
|
||||||
|
Control::Slider { key, default, .. } => {
|
||||||
|
self.slider_state
|
||||||
|
.entry((m.id().to_string(), key))
|
||||||
|
.or_insert(default);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,14 +111,25 @@ impl ControlPanel {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setea un toggle desde afuera (sin emitir evento). Útil cuando el
|
/// Setea un toggle desde afuera (sin emitir evento). Usado por el
|
||||||
/// canvas se autotoggleó via hotkey y queremos sincronizar el panel.
|
/// shell para sincronizar cuando el canvas se autotoggleó via hotkey.
|
||||||
pub fn set_toggle(&mut self, module_id: &str, key: &str, value: bool, cx: &mut Context<Self>) {
|
pub fn set_toggle(&mut self, module_id: &str, key: &str, value: bool, cx: &mut Context<Self>) {
|
||||||
self.toggle_state
|
self.toggle_state
|
||||||
.insert((module_id.to_string(), key.to_string()), value);
|
.insert((module_id.to_string(), key.to_string()), value);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Setea un slider desde afuera (sin emitir evento). El shell la
|
||||||
|
/// usa, por ejemplo, para inicializar `progression.target_age_years`
|
||||||
|
/// con la edad actual del sujeto al cargar una carta nueva.
|
||||||
|
pub fn set_slider(&mut self, module_id: &str, key: &str, value: f64, cx: &mut Context<Self>) {
|
||||||
|
self.slider_state
|
||||||
|
.insert((module_id.to_string(), key.to_string()), value);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- internos: handlers -----
|
||||||
|
|
||||||
fn on_toggle_click(&mut self, module_id: String, key: String, cx: &mut Context<Self>) {
|
fn on_toggle_click(&mut self, module_id: String, key: String, cx: &mut Context<Self>) {
|
||||||
let entry = self
|
let entry = self
|
||||||
.toggle_state
|
.toggle_state
|
||||||
@@ -107,13 +144,83 @@ impl ControlPanel {
|
|||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_slider_drag(
|
||||||
|
&mut self,
|
||||||
|
module_id: String,
|
||||||
|
key: String,
|
||||||
|
min: f64,
|
||||||
|
max: f64,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.slider_drag = Some(SliderDrag {
|
||||||
|
module_id: module_id.clone(),
|
||||||
|
key: key.clone(),
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
});
|
||||||
|
self.apply_slider_position(bounds, position, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn continue_slider_drag(
|
||||||
|
&mut self,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.slider_drag.is_some() {
|
||||||
|
self.apply_slider_position(bounds, position, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_slider_drag(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.slider_drag.take().is_some() {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_slider_position(
|
||||||
|
&mut self,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(drag) = self.slider_drag.as_ref().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let track_x: f32 = bounds.origin.x.into();
|
||||||
|
let track_w: f32 = bounds.size.width.into();
|
||||||
|
let mouse_x: f32 = position.x.into();
|
||||||
|
let fraction = if track_w > 0.0 {
|
||||||
|
((mouse_x - track_x) / track_w).clamp(0.0, 1.0) as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let value = drag.min + fraction * (drag.max - drag.min);
|
||||||
|
self.slider_state
|
||||||
|
.insert((drag.module_id.clone(), drag.key.clone()), value);
|
||||||
|
cx.emit(PanelEvent::ControlChanged {
|
||||||
|
module_id: drag.module_id,
|
||||||
|
key: drag.key,
|
||||||
|
value: serde_json::json!(value),
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Render
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
const SLIDER_TRACK_W: f32 = 140.0;
|
||||||
|
const SLIDER_TRACK_H: f32 = 8.0;
|
||||||
|
const SLIDER_THUMB: f32 = 12.0;
|
||||||
|
|
||||||
impl Render for ControlPanel {
|
impl Render for ControlPanel {
|
||||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let theme = Theme::global(cx).clone();
|
let theme = Theme::global(cx).clone();
|
||||||
// Snapshot de los módulos a renderear — borrowing isssues si
|
|
||||||
// dejáramos el iterador vivo mientras mutamos en el closure.
|
|
||||||
let modules: Vec<(String, String, String, Vec<Control>)> = match self.active_kind {
|
let modules: Vec<(String, String, String, Vec<Control>)> = match self.active_kind {
|
||||||
Some(k) => self
|
Some(k) => self
|
||||||
.registry
|
.registry
|
||||||
@@ -221,7 +328,7 @@ impl ControlPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.min_w(px(240.0))
|
.min_w(px(260.0))
|
||||||
.p(px(8.0))
|
.p(px(8.0))
|
||||||
.rounded(px(6.0))
|
.rounded(px(6.0))
|
||||||
.bg(theme.bg_panel_alt.clone())
|
.bg(theme.bg_panel_alt.clone())
|
||||||
@@ -247,12 +354,35 @@ impl ControlPanel {
|
|||||||
label,
|
label,
|
||||||
default,
|
default,
|
||||||
hotkey,
|
hotkey,
|
||||||
} => {
|
} => self.render_toggle(theme, module_id, key, label, *default, hotkey.as_deref(), cx),
|
||||||
|
Control::Slider {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
default,
|
||||||
|
..
|
||||||
|
} => self.render_slider(theme, module_id, key, label, *min, *max, *default, cx),
|
||||||
|
Control::Select { label, default, .. } => display_row(theme, label, default),
|
||||||
|
Control::TextInput { label, default, .. } => display_row(theme, label, default),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_toggle(
|
||||||
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
module_id: &str,
|
||||||
|
key: &str,
|
||||||
|
label: &str,
|
||||||
|
default: bool,
|
||||||
|
hotkey: Option<&str>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> gpui::Div {
|
||||||
let active = self
|
let active = self
|
||||||
.toggle_state
|
.toggle_state
|
||||||
.get(&(module_id.to_string(), key.clone()))
|
.get(&(module_id.to_string(), key.to_string()))
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(*default);
|
.unwrap_or(default);
|
||||||
let dot_color = if active {
|
let dot_color = if active {
|
||||||
theme.accent
|
theme.accent
|
||||||
} else {
|
} else {
|
||||||
@@ -260,7 +390,10 @@ impl ControlPanel {
|
|||||||
};
|
};
|
||||||
let id_str: SharedString =
|
let id_str: SharedString =
|
||||||
SharedString::from(format!("tts-toggle-{}-{}", module_id, key));
|
SharedString::from(format!("tts-toggle-{}-{}", module_id, key));
|
||||||
let id_for_listener = (module_id.to_string(), key.clone());
|
let id_for_listener = (module_id.to_string(), key.to_string());
|
||||||
|
let hotkey_str = hotkey
|
||||||
|
.map(|h| format!("[{}]", h))
|
||||||
|
.unwrap_or_default();
|
||||||
let row = div()
|
let row = div()
|
||||||
.id(gpui::ElementId::from(id_str))
|
.id(gpui::ElementId::from(id_str))
|
||||||
.flex()
|
.flex()
|
||||||
@@ -276,35 +409,170 @@ impl ControlPanel {
|
|||||||
div()
|
div()
|
||||||
.text_size(px(11.0))
|
.text_size(px(11.0))
|
||||||
.text_color(theme.fg_text)
|
.text_color(theme.fg_text)
|
||||||
.child(SharedString::from(label.clone())),
|
.child(SharedString::from(label.to_string())),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.ml_auto()
|
.ml_auto()
|
||||||
.text_size(px(10.0))
|
.text_size(px(10.0))
|
||||||
.text_color(theme.fg_muted)
|
.text_color(theme.fg_muted)
|
||||||
.child(SharedString::from(
|
.child(SharedString::from(hotkey_str)),
|
||||||
hotkey
|
|
||||||
.clone()
|
|
||||||
.map(|h| format!("[{}]", h))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||||
let (m, k) = id_for_listener.clone();
|
let (m, k) = id_for_listener.clone();
|
||||||
this.on_toggle_click(m, k, cx);
|
this.on_toggle_click(m, k, cx);
|
||||||
}));
|
}));
|
||||||
// `id()` devuelve `Stateful<Div>`; envolvemos para
|
|
||||||
// mantener uniforme el return type del match.
|
|
||||||
div().child(row)
|
div().child(row)
|
||||||
}
|
}
|
||||||
Control::Slider {
|
|
||||||
label,
|
fn render_slider(
|
||||||
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
module_id: &str,
|
||||||
|
key: &str,
|
||||||
|
label: &str,
|
||||||
|
min: f64,
|
||||||
|
max: f64,
|
||||||
|
default: f64,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> gpui::Div {
|
||||||
|
let value = self
|
||||||
|
.slider_state
|
||||||
|
.get(&(module_id.to_string(), key.to_string()))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(default);
|
||||||
|
let range = (max - min).max(f64::EPSILON);
|
||||||
|
let fraction = ((value - min) / range).clamp(0.0, 1.0) as f32;
|
||||||
|
let filled_w = fraction * SLIDER_TRACK_W;
|
||||||
|
let thumb_x = (fraction * SLIDER_TRACK_W) - SLIDER_THUMB / 2.0;
|
||||||
|
|
||||||
|
let entity = cx.entity();
|
||||||
|
let mod_for_mouse = module_id.to_string();
|
||||||
|
let key_for_mouse = key.to_string();
|
||||||
|
let canvas_overlay = canvas(
|
||||||
|
move |_bounds: Bounds<Pixels>, _w, _cx| (),
|
||||||
|
move |bounds: Bounds<Pixels>, _, window, _| {
|
||||||
|
// MouseDown sobre el track → start drag + valor inmediato.
|
||||||
|
let entity_d = entity.clone();
|
||||||
|
let mod_d = mod_for_mouse.clone();
|
||||||
|
let key_d = key_for_mouse.clone();
|
||||||
|
window.on_mouse_event(move |ev: &MouseDownEvent, _, _w, cx| {
|
||||||
|
if ev.button != MouseButton::Left {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !bounds.contains(&ev.position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity_d.update(cx, |this, cx| {
|
||||||
|
this.start_slider_drag(
|
||||||
|
mod_d.clone(),
|
||||||
|
key_d.clone(),
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
default,
|
bounds,
|
||||||
..
|
ev.position,
|
||||||
} => div()
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// MouseMove (durante drag) → continuar solo si ESTE
|
||||||
|
// slider es el que está bajo drag.
|
||||||
|
let entity_m = entity.clone();
|
||||||
|
let mod_m = mod_for_mouse.clone();
|
||||||
|
let key_m = key_for_mouse.clone();
|
||||||
|
window.on_mouse_event(move |ev: &MouseMoveEvent, _, _w, cx| {
|
||||||
|
if !ev.dragging() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity_m.update(cx, |this, cx| {
|
||||||
|
let is_mine = this
|
||||||
|
.slider_drag
|
||||||
|
.as_ref()
|
||||||
|
.map(|d| d.module_id == mod_m && d.key == key_m)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_mine {
|
||||||
|
this.continue_slider_drag(bounds, ev.position, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// MouseUp anywhere → terminar drag.
|
||||||
|
let entity_u = entity.clone();
|
||||||
|
window.on_mouse_event(move |_: &MouseUpEvent, _, _w, cx| {
|
||||||
|
entity_u.update(cx, |this, cx| this.end_slider_drag(cx));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.w(px(SLIDER_TRACK_W))
|
||||||
|
.h(px(SLIDER_TRACK_H));
|
||||||
|
|
||||||
|
let track = div()
|
||||||
|
.relative()
|
||||||
|
.w(px(SLIDER_TRACK_W))
|
||||||
|
.h(px(SLIDER_TRACK_H))
|
||||||
|
.bg(theme.bg_input())
|
||||||
|
.rounded(px(SLIDER_TRACK_H / 2.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(0.0))
|
||||||
|
.top(px(0.0))
|
||||||
|
.h(px(SLIDER_TRACK_H))
|
||||||
|
.w(px(filled_w))
|
||||||
|
.bg(theme.accent)
|
||||||
|
.rounded(px(SLIDER_TRACK_H / 2.0)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(thumb_x))
|
||||||
|
.top(px(-2.0))
|
||||||
|
.w(px(SLIDER_THUMB))
|
||||||
|
.h(px(SLIDER_THUMB))
|
||||||
|
.rounded(px(SLIDER_THUMB / 2.0))
|
||||||
|
.bg(theme.fg_text)
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.border_strong),
|
||||||
|
)
|
||||||
|
.child(canvas_overlay);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(4.0))
|
||||||
|
.px(px(6.0))
|
||||||
|
.py(px(3.0))
|
||||||
|
.child(
|
||||||
|
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.to_string())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.ml_auto()
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"{:.1} ({}…{})",
|
||||||
|
value, min, max
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_row(theme: &Theme, label: &str, value: &str) -> gpui::Div {
|
||||||
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
.flex_row()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -315,55 +583,13 @@ impl ControlPanel {
|
|||||||
div()
|
div()
|
||||||
.text_size(px(11.0))
|
.text_size(px(11.0))
|
||||||
.text_color(theme.fg_text)
|
.text_color(theme.fg_text)
|
||||||
.child(SharedString::from(label.clone())),
|
.child(SharedString::from(label.to_string())),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.ml_auto()
|
.ml_auto()
|
||||||
.text_size(px(10.0))
|
.text_size(px(10.0))
|
||||||
.text_color(theme.fg_muted)
|
.text_color(theme.fg_muted)
|
||||||
.child(SharedString::from(format!("{} ({}…{})", default, min, max))),
|
.child(SharedString::from(value.to_string())),
|
||||||
),
|
|
||||||
Control::Select { label, default, .. } => div()
|
|
||||||
.flex()
|
|
||||||
.flex_row()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(8.0))
|
|
||||||
.px(px(6.0))
|
|
||||||
.py(px(3.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))
|
|
||||||
.px(px(6.0))
|
|
||||||
.py(px(3.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