feat(dominium): backend GPUI + app — ventana viva del simulador
dominium-canvas-gpui: Element que pinta un RenderPlan como quads, centrado en sus bounds (rgba→hsla, único crate que toca gpui). app dominium: compone core→physics→iso→render-plan→canvas en una ventana GPUI con bucle de simulación de fondo (~11 tps), panel de estadísticas, controles play/pausa + re-sembrar, y re-siembra automática al colapso poblacional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+22
@@ -3475,6 +3475,28 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dominium"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dominium-canvas-gpui",
|
||||||
|
"dominium-core",
|
||||||
|
"dominium-iso",
|
||||||
|
"dominium-physics",
|
||||||
|
"dominium-render-plan",
|
||||||
|
"gpui",
|
||||||
|
"nahual-launcher",
|
||||||
|
"nahual-theme",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dominium-canvas-gpui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dominium-render-plan",
|
||||||
|
"gpui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dominium-core"
|
name = "dominium-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ members = [
|
|||||||
"crates/modules/dominium/dominium-physics",
|
"crates/modules/dominium/dominium-physics",
|
||||||
"crates/modules/dominium/dominium-iso",
|
"crates/modules/dominium/dominium-iso",
|
||||||
"crates/modules/dominium/dominium-render-plan",
|
"crates/modules/dominium/dominium-render-plan",
|
||||||
|
"crates/modules/dominium/dominium-canvas-gpui",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
||||||
@@ -223,6 +224,7 @@ members = [
|
|||||||
"crates/apps/cosmobiologia",
|
"crates/apps/cosmobiologia",
|
||||||
"crates/apps/cosmobiologia-cli",
|
"crates/apps/cosmobiologia-cli",
|
||||||
"crates/apps/cosmobiologia-server",
|
"crates/apps/cosmobiologia-server",
|
||||||
|
"crates/apps/dominium",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "dominium"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "dominium — simulador psicológico de campo medio: ventana GPUI con maqueta isométrica viva, panel de estadísticas y bucle de simulación."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "dominium"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dominium-core = { path = "../../modules/dominium/dominium-core" }
|
||||||
|
dominium-physics = { path = "../../modules/dominium/dominium-physics" }
|
||||||
|
dominium-iso = { path = "../../modules/dominium/dominium-iso" }
|
||||||
|
dominium-render-plan = { path = "../../modules/dominium/dominium-render-plan" }
|
||||||
|
dominium-canvas-gpui = { path = "../../modules/dominium/dominium-canvas-gpui" }
|
||||||
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
|
gpui = { workspace = true }
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
//! `dominium` — la ventana viva del simulador de campo medio.
|
||||||
|
//!
|
||||||
|
//! Compone toda la cadena de dominium en un app GPUI:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! dominium-core ─► dominium-physics ─► dominium-iso ─►
|
||||||
|
//! dominium-render-plan ─► dominium-canvas-gpui ─► [esta ventana]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Un bucle de fondo avanza la simulación ~11 veces por segundo; cada
|
||||||
|
//! tick reconstruye la maqueta isométrica y la repinta. El panel
|
||||||
|
//! derecho muestra las estadísticas agregadas y dos controles
|
||||||
|
//! (play/pausa, re-sembrar). Cuando la población colapsa, el mundo se
|
||||||
|
//! re-siembra solo: la demo nunca se queda en negro.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dominium_canvas_gpui::DominiumCanvas;
|
||||||
|
use dominium_core::{SimParams, World};
|
||||||
|
use dominium_iso::{IsoProjector, ZWeights};
|
||||||
|
use dominium_physics::tick;
|
||||||
|
use dominium_render_plan::{build_plan, PlanConfig};
|
||||||
|
use gpui::{
|
||||||
|
div, hsla, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
|
||||||
|
};
|
||||||
|
use nahual_launcher::launch_app;
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
|
||||||
|
/// Lado de la grilla cuadrada del mundo.
|
||||||
|
const GRID: usize = 40;
|
||||||
|
/// Población inicial de Lemmings.
|
||||||
|
const LEMMINGS: usize = 50;
|
||||||
|
/// Periodo del bucle de simulación.
|
||||||
|
const TICK_MS: u64 = 90;
|
||||||
|
|
||||||
|
/// PRNG mínimo (LCG de 64 bits) — siembra reproducible sin dependencias.
|
||||||
|
struct Lcg(u64);
|
||||||
|
|
||||||
|
impl Lcg {
|
||||||
|
fn new(seed: u64) -> Self {
|
||||||
|
Self(seed)
|
||||||
|
}
|
||||||
|
fn next_u32(&mut self) -> u32 {
|
||||||
|
// Constantes de Knuth (MMIX).
|
||||||
|
self.0 = self
|
||||||
|
.0
|
||||||
|
.wrapping_mul(6364136223846793005)
|
||||||
|
.wrapping_add(1442695040888963407);
|
||||||
|
(self.0 >> 33) as u32
|
||||||
|
}
|
||||||
|
/// Flotante uniforme en `[0, 1)`.
|
||||||
|
fn next_f32(&mut self) -> f32 {
|
||||||
|
(self.next_u32() >> 8) as f32 / (1u32 << 24) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Siembra un mundo: continentes de `materia`, vetas de `oro`, niebla de
|
||||||
|
/// `psique` y una población de Lemmings con sesgos y acciones variadas.
|
||||||
|
fn seed(seed: u64) -> World {
|
||||||
|
let mut w = World::new(GRID, GRID);
|
||||||
|
let mut rng = Lcg::new(seed);
|
||||||
|
for cy in 0..GRID {
|
||||||
|
for cx in 0..GRID {
|
||||||
|
let idx = w.grid.idx(cx, cy);
|
||||||
|
// m² concentra la materia en parches → aspecto de continentes.
|
||||||
|
let m = rng.next_f32();
|
||||||
|
w.grid.materia[idx] = m * m * 60.0;
|
||||||
|
if rng.next_f32() > 0.92 {
|
||||||
|
w.grid.oro[idx] = rng.next_f32() * 40.0;
|
||||||
|
}
|
||||||
|
w.grid.psique[idx] = rng.next_f32() * 12.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _ in 0..LEMMINGS {
|
||||||
|
let x = rng.next_f32() * (GRID as f32 - 1.0);
|
||||||
|
let y = rng.next_f32() * (GRID as f32 - 1.0);
|
||||||
|
let psi = [
|
||||||
|
rng.next_f32(),
|
||||||
|
rng.next_f32(),
|
||||||
|
rng.next_f32(),
|
||||||
|
rng.next_f32(),
|
||||||
|
];
|
||||||
|
let i = w.lemmings.spawn(x, y, 30.0 + rng.next_f32() * 40.0, psi);
|
||||||
|
w.lemmings.accion[i] = (rng.next_u32() % 6) as u8;
|
||||||
|
}
|
||||||
|
w
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estadísticas agregadas de un instante de la simulación.
|
||||||
|
struct Stats {
|
||||||
|
poblacion: usize,
|
||||||
|
materia: f32,
|
||||||
|
oro: f32,
|
||||||
|
energia: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El estado del simulador y su presentación.
|
||||||
|
struct Sim {
|
||||||
|
world: World,
|
||||||
|
params: SimParams,
|
||||||
|
iso: IsoProjector,
|
||||||
|
weights: ZWeights,
|
||||||
|
cfg: PlanConfig,
|
||||||
|
running: bool,
|
||||||
|
/// Ticks transcurridos en la época actual.
|
||||||
|
tick: u64,
|
||||||
|
/// Cuántas veces se re-sembró el mundo (colapso poblacional).
|
||||||
|
epoch: u64,
|
||||||
|
/// Semilla rodante para cada re-siembra.
|
||||||
|
rng_seed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sim {
|
||||||
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let rng_seed = 0xD0_31_31_07;
|
||||||
|
let sim = Self {
|
||||||
|
world: seed(rng_seed),
|
||||||
|
params: SimParams::default(),
|
||||||
|
iso: IsoProjector::new(12.0, 0.05),
|
||||||
|
weights: ZWeights::default(),
|
||||||
|
cfg: PlanConfig {
|
||||||
|
tile: 15.0,
|
||||||
|
lemming_size: 8.0,
|
||||||
|
lemming_lift: 0.7,
|
||||||
|
palette: Default::default(),
|
||||||
|
},
|
||||||
|
running: true,
|
||||||
|
tick: 0,
|
||||||
|
epoch: 0,
|
||||||
|
rng_seed,
|
||||||
|
};
|
||||||
|
sim.start_loop(cx);
|
||||||
|
sim
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lanza el bucle de fondo que avanza la simulación.
|
||||||
|
fn start_loop(&self, cx: &mut Context<Self>) {
|
||||||
|
cx.spawn(async move |this, cx| loop {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(TICK_MS))
|
||||||
|
.await;
|
||||||
|
let alive = this.update(cx, |sim, cx| {
|
||||||
|
if sim.running {
|
||||||
|
sim.advance();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if alive.is_err() {
|
||||||
|
break; // la entidad murió → ventana cerrada.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un paso de simulación; re-siembra si la población colapsa.
|
||||||
|
fn advance(&mut self) {
|
||||||
|
tick(&mut self.world, &self.params);
|
||||||
|
self.tick += 1;
|
||||||
|
if self.world.lemmings.is_empty() {
|
||||||
|
self.epoch += 1;
|
||||||
|
self.rng_seed = self.rng_seed.wrapping_mul(2862933555777941757).wrapping_add(1);
|
||||||
|
self.world = seed(self.rng_seed);
|
||||||
|
self.tick = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-siembra el mundo a mano (botón ↺).
|
||||||
|
fn reseed(&mut self) {
|
||||||
|
self.rng_seed = self.rng_seed.wrapping_add(0x9E3779B9);
|
||||||
|
self.world = seed(self.rng_seed);
|
||||||
|
self.tick = 0;
|
||||||
|
self.epoch += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calcula las estadísticas del instante actual.
|
||||||
|
fn stats(&self) -> Stats {
|
||||||
|
let g = &self.world.grid;
|
||||||
|
Stats {
|
||||||
|
poblacion: self.world.lemmings.len(),
|
||||||
|
materia: g.materia.iter().sum(),
|
||||||
|
oro: g.oro.iter().sum(),
|
||||||
|
energia: self.world.lemmings.energia.iter().sum(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fila etiqueta/valor del panel de estadísticas.
|
||||||
|
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.justify_between()
|
||||||
|
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
|
||||||
|
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Sim {
|
||||||
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let theme = Theme::global(cx).clone();
|
||||||
|
let panel = hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
|
||||||
|
let chip = hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
|
||||||
|
let canvas_bg = hsla(220.0 / 360.0, 0.22, 0.06, 1.0);
|
||||||
|
let accent = theme.accent;
|
||||||
|
let stats = self.stats();
|
||||||
|
|
||||||
|
// --- Barra de estado ---
|
||||||
|
let estado = if self.running { "● corriendo" } else { "‖ en pausa" };
|
||||||
|
let status = div()
|
||||||
|
.h(px(34.))
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.px(px(14.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"dominium · campo medio · época {} · tick {}",
|
||||||
|
self.epoch, self.tick
|
||||||
|
)))
|
||||||
|
.child(div().text_color(accent).child(SharedString::from(estado.to_string())));
|
||||||
|
|
||||||
|
// --- Maqueta isométrica ---
|
||||||
|
let plan = build_plan(&self.world, &self.iso, &self.weights, &self.cfg);
|
||||||
|
let canvas = div()
|
||||||
|
.flex_1()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(DominiumCanvas::new(plan).background(canvas_bg));
|
||||||
|
|
||||||
|
// --- Botones de control ---
|
||||||
|
let play_label = if self.running { "‖ Pausar" } else { "▶ Reanudar" };
|
||||||
|
let play = div()
|
||||||
|
.id("play")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child(SharedString::from(play_label.to_string()))
|
||||||
|
.on_click(cx.listener(|sim, _ev, _w, cx| {
|
||||||
|
sim.running = !sim.running;
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
let reset = div()
|
||||||
|
.id("reset")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("↺ Re-sembrar")
|
||||||
|
.on_click(cx.listener(|sim, _ev, _w, cx| {
|
||||||
|
sim.reseed();
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Panel de estadísticas ---
|
||||||
|
let side = div()
|
||||||
|
.w(px(216.))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.))
|
||||||
|
.p(px(12.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(div().text_color(theme.fg_muted).child("[SIM]"))
|
||||||
|
.child(play)
|
||||||
|
.child(reset)
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(stat_row("Población", format!("{}", stats.poblacion), &theme))
|
||||||
|
.child(stat_row("Materia", format!("{:.0}", stats.materia), &theme))
|
||||||
|
.child(stat_row("Oro", format!("{:.0}", stats.oro), &theme))
|
||||||
|
.child(stat_row("Energía", format!("{:.0}", stats.energia), &theme))
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("grilla {GRID}×{GRID}"))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child("relieve = materia (Z)"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Composición ---
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(status)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.flex_1()
|
||||||
|
.child(canvas)
|
||||||
|
.child(side),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch_app("brahman · dominium", (1120., 720.), Sim::new);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "dominium-canvas-gpui"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "dominium — backend GPUI: un Element que pinta un RenderPlan isométrico como quads, centrado en sus bounds. El único crate de dominium que toca gpui."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dominium-render-plan = { path = "../dominium-render-plan" }
|
||||||
|
gpui = { workspace = true }
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
//! `dominium-canvas-gpui` — el único crate de dominium que importa `gpui`.
|
||||||
|
//!
|
||||||
|
//! Toda la cadena `dominium-core → physics → iso → render-plan` es
|
||||||
|
//! agnóstica de backend. Este crate cierra el circuito: un [`Element`]
|
||||||
|
//! de GPUI que recibe un [`RenderPlan`] ya resuelto y lo vuelca a
|
||||||
|
//! `paint_quad`, centrando la maqueta en los bounds disponibles.
|
||||||
|
//!
|
||||||
|
//! Si mañana el frontend fuera web o TUI, se escribe un
|
||||||
|
//! `dominium-canvas-web` hermano sin tocar una línea del núcleo.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
use dominium_render_plan::{Color, RenderPlan};
|
||||||
|
use gpui::{
|
||||||
|
fill, hsla, point, px, size, App, Bounds, Element, ElementId, GlobalElementId, Hsla,
|
||||||
|
InspectorElementId, IntoElement, LayoutId, Pixels, Style, Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Convierte un color RGBA lineal (`[f32;4]`) a `Hsla`, que es lo que
|
||||||
|
/// GPUI consume. Misma convención de conversión que el backend de
|
||||||
|
/// `pineal` — sin gamma.
|
||||||
|
pub fn rgba_to_hsla(c: Color) -> Hsla {
|
||||||
|
let (r, g, b, a) = (c[0], c[1], c[2], c[3]);
|
||||||
|
let max = r.max(g).max(b);
|
||||||
|
let min = r.min(g).min(b);
|
||||||
|
let l = (max + min) * 0.5;
|
||||||
|
let delta = max - min;
|
||||||
|
if delta.abs() < 1e-6 {
|
||||||
|
return hsla(0.0, 0.0, l, a);
|
||||||
|
}
|
||||||
|
let s = if l < 0.5 {
|
||||||
|
delta / (max + min)
|
||||||
|
} else {
|
||||||
|
delta / (2.0 - max - min)
|
||||||
|
};
|
||||||
|
let h = if max == r {
|
||||||
|
((g - b) / delta).rem_euclid(6.0)
|
||||||
|
} else if max == g {
|
||||||
|
(b - r) / delta + 2.0
|
||||||
|
} else {
|
||||||
|
(r - g) / delta + 4.0
|
||||||
|
};
|
||||||
|
hsla(h / 6.0, s, l, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Element` GPUI que pinta una maqueta isométrica.
|
||||||
|
///
|
||||||
|
/// Construir uno nuevo en cada `render()` del host con el `RenderPlan`
|
||||||
|
/// del frame actual — el Element no guarda estado entre frames.
|
||||||
|
pub struct DominiumCanvas {
|
||||||
|
plan: RenderPlan,
|
||||||
|
background: Option<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DominiumCanvas {
|
||||||
|
/// Envuelve un `RenderPlan` listo para pintar.
|
||||||
|
pub fn new(plan: RenderPlan) -> Self {
|
||||||
|
Self { plan, background: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pinta un fondo sólido antes de los quads.
|
||||||
|
pub fn background(mut self, color: Hsla) -> Self {
|
||||||
|
self.background = Some(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for DominiumCanvas {
|
||||||
|
type Element = Self;
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for DominiumCanvas {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
|
||||||
|
style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
|
||||||
|
(window.request_layout(style, [], cx), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&InspectorElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_layout: &mut Self::RequestLayoutState,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&InspectorElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_layout: &mut Self::RequestLayoutState,
|
||||||
|
_prepaint: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
_cx: &mut App,
|
||||||
|
) {
|
||||||
|
let ox: f32 = bounds.origin.x.into();
|
||||||
|
let oy: f32 = bounds.origin.y.into();
|
||||||
|
let bw: f32 = bounds.size.width.into();
|
||||||
|
let bh: f32 = bounds.size.height.into();
|
||||||
|
|
||||||
|
if let Some(bg) = self.background {
|
||||||
|
window.paint_quad(fill(bounds, bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centra la maqueta: el centro de la caja envolvente del plan
|
||||||
|
// se alinea con el centro de los bounds del Element.
|
||||||
|
let plan_cx = (self.plan.min_x + self.plan.max_x) * 0.5;
|
||||||
|
let plan_cy = (self.plan.min_y + self.plan.max_y) * 0.5;
|
||||||
|
let off_x = ox + bw * 0.5 - plan_cx;
|
||||||
|
let off_y = oy + bh * 0.5 - plan_cy;
|
||||||
|
|
||||||
|
// Los quads ya vienen ordenados de atrás hacia adelante.
|
||||||
|
for q in &self.plan.quads {
|
||||||
|
let rect = Bounds {
|
||||||
|
origin: point(px(q.x + off_x), px(q.y + off_y)),
|
||||||
|
size: size(px(q.w), px(q.h)),
|
||||||
|
};
|
||||||
|
window.paint_quad(fill(rect, rgba_to_hsla(q.color)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pure_red_maps_to_hue_zero() {
|
||||||
|
let h = rgba_to_hsla([1.0, 0.0, 0.0, 1.0]);
|
||||||
|
assert!((h.h - 0.0).abs() < 1e-6);
|
||||||
|
assert!((h.s - 1.0).abs() < 1e-6);
|
||||||
|
assert!((h.l - 0.5).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grey_has_zero_saturation() {
|
||||||
|
let h = rgba_to_hsla([0.4, 0.4, 0.4, 0.8]);
|
||||||
|
assert!((h.s - 0.0).abs() < 1e-6);
|
||||||
|
assert!((h.a - 0.8).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_passes_through() {
|
||||||
|
let h = rgba_to_hsla([0.0, 0.0, 1.0, 0.25]);
|
||||||
|
assert!((h.a - 0.25).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user