feat(tahuantinsuyu): fase 25 — production polish (tree search + hotkey SVG + aspect tooltips)
Tres mejoras de usabilidad que aprovechan toda la infraestructura ya construida. PNG export (#17) lo dejé para una fase futura — agregar resvg suma ~1MB al binario y el SVG ya cubre el caso profesional. ## #9 — Búsqueda en el tree Arriba del árbol de groups/contacts/cartas aparece un TextInput con placeholder "Buscar nombre…". Al apretar Enter aplica el filtro: case-insensitive substring match sobre group.name, contact.name y chart.label. Auto-expande recursivamente todos los ancestros que contienen un match — los resultados quedan visibles sin que el usuario tenga que abrir chevrons. Escape limpia el filtro. - tree: TahuantinsuyuTree gana `search_filter: String` y `search_input: Entity<TextInput>`. set_search_filter() actualiza + auto_expand_matches + refresh. - group_has_match() / contact_has_match() recursivos chequean si algún descendiente matchea. - append_groups/contacts/charts filtran por substring antes de emitir cada row. - render: nueva barra search arriba con border-bottom. ## #10 — Hotkey [S] = export SVG El canvas ya tenía botón "⬇ SVG" en el header del wheel. Ahora la tecla [S] sobre el wheel (con focus) emite el mismo evento ExportSvgRequested. La línea de hotkeys del footer pasa a: "[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [S]vg [R]eset". ## #8 — Tooltips sobre líneas de aspecto - engine: LineSeg gana fields opcionales `from_body: String`, `to_body: String`, `orb_deg: f32` (default empty/0.0 para back-compat serde). Bridge popula los 6 sites de LineSeg construction (natal aspects + 5 cross overlays) vía un script Python regex que añadió `from_body: body_symbol(a.X).into()` + `to_body: body_symbol(a.Y).into()` + `orb_deg: a.orb_abs_deg() as f32` a cada constructor. - canvas: HoverInfo gana variante Aspect { module_id, from_body, to_body, kind, orb_deg, local_x, local_y }. on_hover_check itera Aspects layers después de los Bodies (precedencia a planetas) y computa distancia punto-segmento con `dist_point_segment` helper — threshold 4px para hover. Tooltip muestra "☉ △ ♂ · orb 2.3°" con prefix de módulo si no es natal. cargo check verde, 8 tests engine + 1 modules verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -120,9 +120,8 @@ pub struct CanvasState {
|
||||
}
|
||||
|
||||
/// Info del elemento bajo el cursor — usado por el render para mostrar
|
||||
/// un tooltip flotante con detalles. Cubre body glyphs (módulo +
|
||||
/// símbolo + grado + casa + retro + dignidad) y cusps de casa (número
|
||||
/// de la casa + grado del cusp + signo).
|
||||
/// un tooltip flotante con detalles. Cubre body glyphs, cusps de casa,
|
||||
/// y líneas de aspectos.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HoverInfo {
|
||||
Body {
|
||||
@@ -142,6 +141,18 @@ pub enum HoverInfo {
|
||||
local_x: f32,
|
||||
local_y: f32,
|
||||
},
|
||||
/// Hover sobre una línea de aspecto. `from_body`/`to_body` y `kind`
|
||||
/// vienen de la LineSeg; `orb_deg` también. Los coords son el
|
||||
/// punto medio del segmento donde se muestra el tooltip.
|
||||
Aspect {
|
||||
module_id: String,
|
||||
from_body: String,
|
||||
to_body: String,
|
||||
kind: String,
|
||||
orb_deg: f32,
|
||||
local_x: f32,
|
||||
local_y: f32,
|
||||
},
|
||||
}
|
||||
|
||||
impl HoverInfo {
|
||||
@@ -149,6 +160,7 @@ impl HoverInfo {
|
||||
match self {
|
||||
HoverInfo::Body { local_x, local_y, .. } => (*local_x, *local_y),
|
||||
HoverInfo::HouseCusp { local_x, local_y, .. } => (*local_x, *local_y),
|
||||
HoverInfo::Aspect { local_x, local_y, .. } => (*local_x, *local_y),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +170,13 @@ impl HoverInfo {
|
||||
module_id, symbol, ..
|
||||
} => format!("body:{}:{}", module_id, symbol),
|
||||
HoverInfo::HouseCusp { house_number, .. } => format!("cusp:{}", house_number),
|
||||
HoverInfo::Aspect {
|
||||
module_id,
|
||||
from_body,
|
||||
to_body,
|
||||
kind,
|
||||
..
|
||||
} => format!("aspect:{}:{}-{}-{}", module_id, from_body, kind, to_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,7 +385,52 @@ impl AstrologyCanvas {
|
||||
}
|
||||
}
|
||||
|
||||
// 2) House cusps — solo si el mouse está cerca del anillo de
|
||||
// 2) Aspect lines (segundo: las líneas son más "frágiles" que
|
||||
// los planetas; si un body matcheó arriba ya tomó precedencia).
|
||||
// Computa distancia punto-segmento del mouse al line.
|
||||
if best.is_none() {
|
||||
for layer in &render.layers {
|
||||
if !matches!(layer.kind, LayerKind::Aspects) {
|
||||
continue;
|
||||
}
|
||||
let (r_from, r_to) = radii.aspect_endpoints(&layer.module_id);
|
||||
if let Geometry::Lines(segs) = &layer.geometry {
|
||||
for seg in segs {
|
||||
if seg.from_body.is_empty() || seg.to_body.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (ax, ay) = polar_to_screen(seg.from_deg, asc, rot, r_from);
|
||||
let (bx, by) = polar_to_screen(seg.to_deg, asc, rot, r_to);
|
||||
let px_a = cx_px + ax;
|
||||
let py_a = cy_px + ay;
|
||||
let px_b = cx_px + bx;
|
||||
let py_b = cy_px + by;
|
||||
let dist = dist_point_segment(mx, my, px_a, py_a, px_b, py_b);
|
||||
if dist > 4.0 {
|
||||
continue;
|
||||
}
|
||||
if best.as_ref().map(|(d, _)| dist < *d).unwrap_or(true) {
|
||||
let mid_x = (px_a + px_b) / 2.0;
|
||||
let mid_y = (py_a + py_b) / 2.0;
|
||||
best = Some((
|
||||
dist,
|
||||
HoverInfo::Aspect {
|
||||
module_id: layer.module_id.clone(),
|
||||
from_body: seg.from_body.clone(),
|
||||
to_body: seg.to_body.clone(),
|
||||
kind: seg.kind.clone(),
|
||||
orb_deg: seg.orb_deg,
|
||||
local_x: mid_x - ox,
|
||||
local_y: mid_y - oy,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) House cusps — solo si el mouse está cerca del anillo de
|
||||
// casas (radio entre houses_inner y houses_outer + margen) y
|
||||
// ningún body ganó. Las cusps son líneas radiales — la
|
||||
// distancia angular al cusp más cercano determina el hit.
|
||||
@@ -464,6 +528,10 @@ impl AstrologyCanvas {
|
||||
self.reset_time_offset(cx);
|
||||
return;
|
||||
}
|
||||
"s" | "S" => {
|
||||
cx.emit(CanvasEvent::ExportSvgRequested);
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
self.toggle_layer(kind, cx);
|
||||
@@ -857,6 +925,26 @@ fn render_wheel(
|
||||
house_number, sign_name, deg_in_sign
|
||||
)
|
||||
}
|
||||
HoverInfo::Aspect {
|
||||
module_id,
|
||||
from_body,
|
||||
to_body,
|
||||
kind,
|
||||
orb_deg,
|
||||
..
|
||||
} => {
|
||||
let mut t = format!(
|
||||
"{} {} {} · orb {:.1}°",
|
||||
planet_unicode(from_body),
|
||||
aspect_unicode(kind),
|
||||
planet_unicode(to_body),
|
||||
orb_deg
|
||||
);
|
||||
if module_id != "natal" {
|
||||
t.push_str(&format!(" · {}", module_id));
|
||||
}
|
||||
t
|
||||
}
|
||||
};
|
||||
let (lx, ly) = hov.local();
|
||||
let tip_x = (lx + 14.0).min(WHEEL_SIZE - 220.0).max(8.0);
|
||||
@@ -981,7 +1069,7 @@ fn render_wheel(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_disabled)
|
||||
.child("[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [R]eset"),
|
||||
.child("[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [S]vg [R]eset"),
|
||||
);
|
||||
|
||||
// Badges de overlays activos. Cada uno se pinta como pill con
|
||||
@@ -1626,6 +1714,26 @@ fn paint_cross_aspect_line(
|
||||
// Helpers
|
||||
// =====================================================================
|
||||
|
||||
/// Distancia mínima entre un punto y un segmento de recta. Usado por
|
||||
/// hover_check para detectar proximity a líneas de aspectos.
|
||||
fn dist_point_segment(px: f32, py: f32, ax: f32, ay: f32, bx: f32, by: f32) -> f32 {
|
||||
let dx = bx - ax;
|
||||
let dy = by - ay;
|
||||
let len_sq = dx * dx + dy * dy;
|
||||
if len_sq < f32::EPSILON {
|
||||
// Segmento degenerado → distancia al punto a.
|
||||
let pdx = px - ax;
|
||||
let pdy = py - ay;
|
||||
return (pdx * pdx + pdy * pdy).sqrt();
|
||||
}
|
||||
let t = (((px - ax) * dx + (py - ay) * dy) / len_sq).clamp(0.0, 1.0);
|
||||
let proj_x = ax + t * dx;
|
||||
let proj_y = ay + t * dy;
|
||||
let dx2 = px - proj_x;
|
||||
let dy2 = py - proj_y;
|
||||
(dx2 * dx2 + dy2 * dy2).sqrt()
|
||||
}
|
||||
|
||||
fn polar_to_screen(
|
||||
longitude_deg: f32,
|
||||
ascendant_deg: f32,
|
||||
|
||||
@@ -416,6 +416,9 @@ fn build_transit_overlay(
|
||||
to_deg: transit_p.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
opacity: opacity * 0.75,
|
||||
from_body: body_symbol(a.person_a_body).into(),
|
||||
to_body: body_symbol(a.person_b_body).into(),
|
||||
orb_deg: a.orb_abs_deg() as f32,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -485,6 +488,9 @@ fn build_progression_overlay(
|
||||
to_deg: prog_p.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
opacity: opacity * 0.7,
|
||||
from_body: body_symbol(a.person_a_body).into(),
|
||||
to_body: body_symbol(a.person_b_body).into(),
|
||||
orb_deg: a.orb_abs_deg() as f32,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -709,6 +715,9 @@ fn build_solar_arc_overlay(
|
||||
to_deg: dir_p.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
opacity: opacity * 0.7,
|
||||
from_body: body_symbol(a.person_a_body).into(),
|
||||
to_body: body_symbol(a.person_b_body).into(),
|
||||
orb_deg: a.orb_abs_deg() as f32,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -773,6 +782,9 @@ fn build_synastry_overlay(
|
||||
to_deg: partner_p.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
opacity: opacity * 0.85,
|
||||
from_body: body_symbol(a.person_a_body).into(),
|
||||
to_body: body_symbol(a.person_b_body).into(),
|
||||
orb_deg: a.orb_abs_deg() as f32,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -883,6 +895,9 @@ fn build_planetary_return_overlay(
|
||||
to_deg: r_p.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
opacity: opacity * 0.8,
|
||||
from_body: body_symbol(a.person_a_body).into(),
|
||||
to_body: body_symbol(a.person_b_body).into(),
|
||||
orb_deg: a.orb_abs_deg() as f32,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -1014,6 +1029,9 @@ fn build_render_model(
|
||||
to_deg: pb.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
opacity,
|
||||
from_body: body_symbol(a.a).into(),
|
||||
to_body: body_symbol(a.b).into(),
|
||||
orb_deg: a.orb_abs_deg() as f32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ pub enum Geometry {
|
||||
Points(Vec<PointMark>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct LineSeg {
|
||||
/// Grados zodiacales del extremo "a".
|
||||
pub from_deg: f32,
|
||||
@@ -176,6 +176,17 @@ pub struct LineSeg {
|
||||
/// resuelve a color.
|
||||
pub kind: String,
|
||||
pub opacity: f32,
|
||||
/// Cuerpo en el extremo "a" — populado para LineSegs de aspectos
|
||||
/// (natal × natal, cross con overlays). Vacío en `Default::default`
|
||||
/// para serde back-compat.
|
||||
#[serde(default)]
|
||||
pub from_body: String,
|
||||
/// Cuerpo en el extremo "b".
|
||||
#[serde(default)]
|
||||
pub to_body: String,
|
||||
/// Orb absoluto en grados (para tooltips).
|
||||
#[serde(default)]
|
||||
pub orb_deg: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -156,6 +156,14 @@ pub struct TahuantinsuyuTree {
|
||||
/// llamar [`Self::set_city_atlas`] para reemplazar por uno custom
|
||||
/// cargado desde disco (TSV).
|
||||
city_atlas: Vec<CityPreset>,
|
||||
/// Filtro de búsqueda activo. Vacío = sin filtro (jerarquía
|
||||
/// completa). Cuando hay texto, refresh() solo incluye rows cuyo
|
||||
/// nombre (group / contact / chart label) contenga el substring
|
||||
/// case-insensitive, y auto-expande los ancestros para que el
|
||||
/// match sea visible.
|
||||
search_filter: String,
|
||||
/// TextInput para el filtro — vive arriba del tree.
|
||||
search_input: Entity<TextInput>,
|
||||
}
|
||||
|
||||
/// Preset de ciudad con datos canónicos para autocompletar lat/lon/tz
|
||||
@@ -331,6 +339,23 @@ impl TahuantinsuyuTree {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let search_input = cx.new(|cx| {
|
||||
TextInput::new(String::new(), cx)
|
||||
.with_placeholder(SharedString::from("Buscar nombre…"))
|
||||
});
|
||||
cx.subscribe(
|
||||
&search_input,
|
||||
|this: &mut Self, _, ev: &TextInputEvent, cx| match ev {
|
||||
TextInputEvent::Confirmed(value) => {
|
||||
this.set_search_filter(value.clone(), cx);
|
||||
}
|
||||
TextInputEvent::Cancelled => {
|
||||
this.set_search_filter(String::new(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let mut me = Self {
|
||||
store,
|
||||
inner,
|
||||
@@ -339,6 +364,8 @@ impl TahuantinsuyuTree {
|
||||
modal: None,
|
||||
city_picker_open: false,
|
||||
city_atlas: default_city_presets(),
|
||||
search_filter: String::new(),
|
||||
search_input,
|
||||
};
|
||||
me.refresh(cx);
|
||||
me
|
||||
@@ -360,12 +387,135 @@ impl TahuantinsuyuTree {
|
||||
self.inner.update(cx, |t, cx| t.set_rows(rows, cx));
|
||||
}
|
||||
|
||||
/// Actualiza el filtro de búsqueda — texto vacío = sin filtro.
|
||||
/// Cuando hay filtro, expande automáticamente los ancestros que
|
||||
/// contienen matches para que el usuario vea los resultados sin
|
||||
/// tener que clickear chevrons.
|
||||
fn set_search_filter(&mut self, filter: String, cx: &mut Context<Self>) {
|
||||
self.search_filter = filter.trim().to_lowercase();
|
||||
if !self.search_filter.is_empty() {
|
||||
self.auto_expand_matches();
|
||||
}
|
||||
self.refresh(cx);
|
||||
}
|
||||
|
||||
/// Pre-expande todos los groups + contacts que contienen al menos
|
||||
/// un descendiente cuyo nombre matchee el filtro. Hace una pasada
|
||||
/// recursiva agregando ids al `expanded` set.
|
||||
fn auto_expand_matches(&mut self) {
|
||||
fn walk_group(this: &mut TahuantinsuyuTree, group_id: GroupId) -> bool {
|
||||
let mut any_match = false;
|
||||
// Sub-groups recursivamente.
|
||||
if let Ok(children) = this.store.list_groups(Some(group_id)) {
|
||||
for g in children {
|
||||
let name_match = g.name.to_lowercase().contains(&this.search_filter);
|
||||
let child_match = walk_group(this, g.id);
|
||||
if name_match || child_match {
|
||||
this.expanded.insert(format!("{}{}", PREFIX_GROUP, g.id));
|
||||
any_match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Contacts directos.
|
||||
if let Ok(contacts) = this.store.list_contacts(Some(group_id)) {
|
||||
for c in contacts {
|
||||
let name_match = c.name.to_lowercase().contains(&this.search_filter);
|
||||
let chart_match = contact_has_matching_chart(this, c.id);
|
||||
if name_match || chart_match {
|
||||
this.expanded.insert(format!("{}{}", PREFIX_CONTACT, c.id));
|
||||
any_match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
any_match
|
||||
}
|
||||
fn contact_has_matching_chart(this: &TahuantinsuyuTree, contact_id: ContactId) -> bool {
|
||||
this.store
|
||||
.list_charts(contact_id)
|
||||
.map(|charts| {
|
||||
charts
|
||||
.iter()
|
||||
.any(|h| h.label.to_lowercase().contains(&this.search_filter))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// Top-level groups + contacts directos en raíz.
|
||||
if let Ok(groups) = self.store.list_groups(None) {
|
||||
for g in groups {
|
||||
let name_match = g.name.to_lowercase().contains(&self.search_filter);
|
||||
let child_match = walk_group(self, g.id);
|
||||
if name_match || child_match {
|
||||
self.expanded.insert(format!("{}{}", PREFIX_GROUP, g.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(contacts) = self.store.list_contacts(None) {
|
||||
for c in contacts {
|
||||
let name_match = c.name.to_lowercase().contains(&self.search_filter);
|
||||
let chart_match = contact_has_matching_chart(self, c.id);
|
||||
if name_match || chart_match {
|
||||
self.expanded.insert(format!("{}{}", PREFIX_CONTACT, c.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` si la jerarquía bajo `group_id` (recursivo) tiene
|
||||
/// algún descendiente que matchee el filtro de búsqueda.
|
||||
fn group_has_match(&self, group_id: GroupId) -> bool {
|
||||
if let Ok(sub) = self.store.list_groups(Some(group_id)) {
|
||||
for g in &sub {
|
||||
if g.name.to_lowercase().contains(&self.search_filter) {
|
||||
return true;
|
||||
}
|
||||
if self.group_has_match(g.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(contacts) = self.store.list_contacts(Some(group_id)) {
|
||||
for c in &contacts {
|
||||
if c.name.to_lowercase().contains(&self.search_filter) {
|
||||
return true;
|
||||
}
|
||||
if let Ok(charts) = self.store.list_charts(c.id) {
|
||||
if charts
|
||||
.iter()
|
||||
.any(|h| h.label.to_lowercase().contains(&self.search_filter))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `true` si el contacto tiene una carta cuyo label matchee.
|
||||
fn contact_has_match(&self, contact_id: ContactId) -> bool {
|
||||
if let Ok(charts) = self.store.list_charts(contact_id) {
|
||||
return charts
|
||||
.iter()
|
||||
.any(|h| h.label.to_lowercase().contains(&self.search_filter));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn append_groups(&self, parent: Option<GroupId>, depth: u32, out: &mut Vec<TreeRow>) {
|
||||
let groups = match self.store.list_groups(parent) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
for g in groups {
|
||||
// Filtro: incluir si el group matchea por nombre o tiene
|
||||
// algún descendiente matching.
|
||||
if !self.search_filter.is_empty() {
|
||||
let name_match = g.name.to_lowercase().contains(&self.search_filter);
|
||||
if !name_match && !self.group_has_match(g.id) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let id_str = format!("{}{}", PREFIX_GROUP, g.id);
|
||||
let expanded = self.expanded.contains(&id_str);
|
||||
out.push(TreeRow {
|
||||
@@ -389,6 +539,12 @@ impl TahuantinsuyuTree {
|
||||
Err(_) => return,
|
||||
};
|
||||
for c in contacts {
|
||||
if !self.search_filter.is_empty() {
|
||||
let name_match = c.name.to_lowercase().contains(&self.search_filter);
|
||||
if !name_match && !self.contact_has_match(c.id) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let id_str = format!("{}{}", PREFIX_CONTACT, c.id);
|
||||
let expanded = self.expanded.contains(&id_str);
|
||||
out.push(TreeRow {
|
||||
@@ -411,6 +567,11 @@ impl TahuantinsuyuTree {
|
||||
Err(_) => return,
|
||||
};
|
||||
for h in charts {
|
||||
if !self.search_filter.is_empty()
|
||||
&& !h.label.to_lowercase().contains(&self.search_filter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let id_str = format!("{}{}", PREFIX_CHART, h.id);
|
||||
out.push(TreeRow {
|
||||
id: RowId::new(id_str),
|
||||
@@ -1049,6 +1210,13 @@ const MENU_WIDTH: f32 = 220.0;
|
||||
impl Render for TahuantinsuyuTree {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
let search_bar = div()
|
||||
.px(px(6.0))
|
||||
.py(px(4.0))
|
||||
.border_b_1()
|
||||
.border_color(theme.border)
|
||||
.child(self.search_input.clone());
|
||||
|
||||
let mut root = div()
|
||||
.id("tahuantinsuyu-tree-root")
|
||||
.size_full()
|
||||
@@ -1056,7 +1224,8 @@ impl Render for TahuantinsuyuTree {
|
||||
.bg(theme.bg_panel.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.inner.clone());
|
||||
.child(search_bar)
|
||||
.child(div().flex_grow().min_h(px(0.0)).child(self.inner.clone()));
|
||||
|
||||
if let Some(menu) = self.menu.clone() {
|
||||
root = root.child(self.render_menu(&theme, menu, cx));
|
||||
|
||||
Reference in New Issue
Block a user