use concinnity_core::components::TextAlign;
use concinnity_core::ecs::World;
use super::shader_reference::RefRow;
use crate::editor::hud_ids::hud_ids;
use crate::editor::text_area::layout::Metrics;
use crate::editor::theme;
use crate::editor::widget::{self, place_rounded, point_in};
const POOL: usize = 48;
hud_ids! {
base: super::shader_source_panel::REFERENCE_BASE;
sprites: [COLUMN_BG, row_bg[POOL]];
labels: [heading_label[POOL]];
code_labels: [name_label[POOL]];
}
pub(crate) const COLUMN_W: f32 = 250.0;
const ROW_H: f32 = 22.0;
const INSET: f32 = 4.0;
const TEXT_PAD: f32 = 8.0;
const WELL_TINT: [f32; 4] = [0.08, 0.08, 0.10, 1.0];
const HEADING_COLOR: [f32; 3] = [0.58, 0.66, 0.80];
pub(crate) struct RefView<'a> {
pub rows: &'a [RefRow<'a>],
pub scroll: usize,
pub mouse: [f32; 2],
}
pub(crate) fn rows_shown(rect: [f32; 4]) -> usize {
(((rect[3] - 2.0 * INSET) / ROW_H).floor().max(1.0) as usize).min(POOL)
}
fn row_rect(rect: [f32; 4], slot: usize) -> [f32; 4] {
[
rect[0],
rect[1] + INSET + slot as f32 * ROW_H,
rect[2],
ROW_H,
]
}
pub(crate) fn slot_at(rect: [f32; 4], mx: f32, my: f32) -> Option<usize> {
if !point_in(mx, my, rect) || my < rect[1] + INSET {
return None;
}
let slot = ((my - rect[1] - INSET) / ROW_H).floor() as usize;
(slot < rows_shown(rect)).then_some(slot)
}
pub(crate) fn hovered<'v, 'a>(view: &'v RefView<'a>, rect: [f32; 4]) -> Option<&'v RefRow<'a>> {
let slot = slot_at(rect, view.mouse[0], view.mouse[1])?;
view.rows.get(view.scroll + slot)
}
pub(crate) fn place(world: &mut World, view: Option<&RefView>, rect: [f32; 4]) {
let Some(view) = view else {
return ids().hide(world);
};
place_rounded(
world,
COLUMN_BG,
rect,
WELL_TINT,
theme::CONTROL_RADIUS,
true,
);
let m = Metrics::code();
let shown = rows_shown(rect);
let hover = slot_at(rect, view.mouse[0], view.mouse[1]);
for slot in 0..POOL {
let row = (slot < shown)
.then(|| view.rows.get(view.scroll + slot))
.flatten();
let r = row_rect(rect, slot);
let lit = row.is_some() && hover == Some(slot);
place_rounded(
world,
row_bg(slot),
theme::highlight_rect(r),
theme::HOVER_TINT,
theme::CONTROL_RADIUS,
lit,
);
let heading = matches!(row, Some(RefRow::Heading { .. }));
let name = matches!(row, Some(RefRow::Name(_)));
let text = row.map(RefRow::text).unwrap_or_default();
if let Some(l) = widget::label_mut(world, heading_label(slot)) {
l.x = r[0] + TEXT_PAD;
l.y = r[1] + ROW_H * 0.5 - theme::TEXT_HALF;
l.align = TextAlign::Left;
l.color = HEADING_COLOR;
l.visible = heading;
l.content = if heading { text.clone() } else { String::new() };
}
if let Some(l) = widget::label_mut(world, name_label(slot)) {
l.x = r[0] + TEXT_PAD * 2.0;
l.y = r[1] + (ROW_H - m.text_px) * 0.5;
l.align = TextAlign::Left;
l.scale = m.label_scale;
l.color = theme::CODE_ENGINE;
l.wrap_width = (r[2] - TEXT_PAD * 3.0).max(0.0);
l.max_lines = 1;
l.visible = name;
l.content = if name { text } else { String::new() };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::panels::shader_reference::Reference;
use concinnity_core::components::{Sprite, TextLabel};
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::render::shader_programs::vocabulary::ENTRIES;
const RECT: [f32; 4] = [100.0, 50.0, COLUMN_W, 10.0 * ROW_H + 2.0 * INSET];
fn injected_world() -> World {
ids().test_world()
}
fn label(world: &World, id: AssetId) -> TextLabel {
world.get_by_id::<TextLabel>(id).cloned().unwrap()
}
#[test]
fn rows_fill_the_column_from_the_scroll_window() {
let mut world = injected_world();
let rows = Reference::default().rows(ENTRIES);
let view = RefView {
rows: &rows,
scroll: 1,
mouse: [0.0, 0.0],
};
place(&mut world, Some(&view), RECT);
assert_eq!(rows_shown(RECT), 10);
let first = label(&world, name_label(0));
assert!(first.visible && first.content == "shade_surface()");
assert!(!label(&world, heading_label(0)).visible);
assert!(!label(&world, name_label(10)).visible, "past the rect");
}
#[test]
fn a_heading_draws_in_the_heading_face_and_hover_lights_its_row() {
let mut world = injected_world();
let rows = Reference::default().rows(ENTRIES);
let r = row_rect(RECT, 0);
let view = RefView {
rows: &rows,
scroll: 0,
mouse: [r[0] + 5.0, r[1] + 5.0],
};
place(&mut world, Some(&view), RECT);
let heading = label(&world, heading_label(0));
assert!(heading.visible && heading.content.starts_with("- Helpers"));
assert!(!label(&world, name_label(0)).visible);
assert!(world.get_by_id::<Sprite>(row_bg(0)).unwrap().visible);
assert!(!world.get_by_id::<Sprite>(row_bg(1)).unwrap().visible);
assert_eq!(hovered(&view, RECT), Some(&rows[0]));
}
#[test]
fn slot_at_maps_points_to_shown_rows() {
let r = row_rect(RECT, 3);
assert_eq!(slot_at(RECT, r[0] + 1.0, r[1] + 1.0), Some(3));
assert_eq!(slot_at(RECT, RECT[0] - 1.0, r[1] + 1.0), None);
assert_eq!(
slot_at(RECT, RECT[0] + 1.0, RECT[1] + 1.0),
None,
"the inset"
);
}
}