use crate::components::{LabelBox, LayoutContainer, TextLabel};
use crate::ecs::PipelineContext;
use crate::ecs::asset_id::AssetId;
use crate::gfx::text;
#[derive(Debug, Default)]
pub(super) struct LabelLayoutScratch {
boxes: std::collections::HashMap<AssetId, LabelBox>,
placements: Vec<crate::components::LabelPlacement>,
placed: std::collections::HashMap<AssetId, (f32, f32)>,
}
pub(super) fn apply_label_layout(
ctx: &mut PipelineContext,
loaded_fonts: &text::FontSet,
scratch: &mut LabelLayoutScratch,
) {
if !ctx.query::<LayoutContainer>().any(|c| c.visible) {
return;
}
scratch.boxes.clear();
for label in ctx.query::<TextLabel>() {
if let Some(b) = text::measure_label_box(label, loaded_fonts) {
scratch.boxes.insert(label.asset_id, b);
}
}
scratch.placed.clear();
for c in ctx.query::<LayoutContainer>() {
if !c.visible {
continue;
}
let boxes = &scratch.boxes;
c.layout_into(|id| boxes.get(&id).copied(), &mut scratch.placements);
for p in &scratch.placements {
scratch.placed.insert(p.id, (p.x, p.y));
}
}
for label in ctx.query_mut::<TextLabel>() {
if let Some(&(x, y)) = scratch.placed.get(&label.asset_id) {
label.x = x;
label.y = y;
}
}
}
const MARGIN: f32 = 10.0;
enum ChipStrip {
DownFromRight { win_w: f32, gap: f32 },
Rightward { gap: f32 },
}
pub(super) fn position_debug_hud(
ctx: &mut PipelineContext,
chip_ids: &[AssetId],
loaded_fonts: &text::FontSet,
win_w: f32,
) {
if win_w <= 0.0 {
return;
}
let strip = ChipStrip::DownFromRight { win_w, gap: 6.0 };
position_chip_strip(ctx, chip_ids, loaded_fonts, strip);
}
pub(super) fn position_stat_hud(
ctx: &mut PipelineContext,
chip_ids: &[AssetId],
loaded_fonts: &text::FontSet,
) {
position_chip_strip(
ctx,
chip_ids,
loaded_fonts,
ChipStrip::Rightward { gap: 4.0 },
);
}
fn position_chip_strip(
ctx: &mut PipelineContext,
chip_ids: &[AssetId],
loaded_fonts: &text::FontSet,
strip: ChipStrip,
) {
let mut run = MARGIN;
for &id in chip_ids {
let Some(l) = crate::ecs::by_asset_id::find_mut::<TextLabel>(ctx, id) else {
continue;
};
if l.content.is_empty() {
continue;
}
let Some(b) = text::measure_label_box(l, loaded_fonts) else {
continue;
};
match strip {
ChipStrip::DownFromRight { win_w, gap } => {
l.x = (win_w - MARGIN - b.w + b.pad).max(MARGIN);
l.y = run + b.top_inset;
run += b.h + gap;
}
ChipStrip::Rightward { gap } => {
l.x = run + b.pad;
l.y = MARGIN + b.top_inset;
run += b.w + gap;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::blob::BlobData;
use crate::components::{Justify, LayoutRow, SpriteFit, TextAlign};
use crate::ecs::{ComponentSlot, ComponentStorage, FontHandle, Resources};
use crate::gfx::profile::FrameProfile;
const FONT: FontHandle = FontHandle(0);
const SENTINEL: f32 = -999.0;
fn make_glyph(advance_px: f32) -> crate::gfx::font::GlyphMetrics {
crate::gfx::font::GlyphMetrics {
char_code: 0,
atlas_x: 0,
atlas_y: 0,
atlas_w: 8,
atlas_h: 12,
advance_px,
bearing_x: 0.0,
bearing_y: 12.0,
}
}
fn loaded_fonts() -> text::FontSet {
let metrics: crate::gfx::text::FontMetrics = ('a'..='z')
.chain('A'..='Z')
.map(|c| (c as u32, make_glyph(10.0)))
.collect();
let cap_px = text::derive_cap_px(&metrics, 16.0);
let mut fonts = text::FontSet::default();
fonts.insert(
FONT,
text::LoadedFont {
atlas_slot: 0,
cap_px,
metrics,
atlas_w: 128,
atlas_h: 128,
size_px: 16.0,
supersample: 1.0,
},
);
fonts
}
fn chip(id: AssetId, content: &str) -> TextLabel {
TextLabel {
asset_id: id,
font: Some(FONT),
content: content.to_string(),
x: SENTINEL,
y: SENTINEL,
color: [1.0, 1.0, 1.0],
scale: 1.0,
centered: false,
align: TextAlign::Left,
fit: SpriteFit::Fit,
background: [0.0, 0.0, 0.0, 0.0],
padding: 0.0,
visible: true,
screen: None,
wrap_width: 0.0,
max_lines: 0,
}
}
fn row(cols: &[AssetId]) -> LayoutRow {
LayoutRow {
cols: cols.to_vec(),
justify: Justify::Left,
}
}
struct TestWorld {
components: ComponentStorage,
blob: BlobData,
profile: FrameProfile,
resources: Resources,
scratch: crate::ecs::Arena,
}
impl TestWorld {
fn new() -> Self {
Self {
components: ComponentStorage::default(),
blob: BlobData::new(vec![Some(Vec::new())]),
profile: FrameProfile::default(),
resources: Resources::new(),
scratch: crate::ecs::Arena::with_capacity(64 * 1024),
}
}
fn push<C: ComponentSlot>(&mut self, c: C) {
self.components.push_typed(c);
}
fn ctx(&mut self) -> PipelineContext<'_> {
PipelineContext {
components: &mut self.components,
blob: &mut self.blob,
profile: &mut self.profile,
resources: &mut self.resources,
frame: crate::ecs::FrameContext::new(&self.scratch),
}
}
fn label_at(&mut self, id: AssetId) -> (f32, f32) {
let ctx = self.ctx();
let l = ctx
.query::<TextLabel>()
.find(|l| l.asset_id == id)
.expect("label pushed");
(l.x, l.y)
}
}
#[test]
fn apply_label_layout_places_rows_from_the_container_origin() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
w.push(chip(AssetId(2), "aaaa"));
w.push(chip(AssetId(3), "aaaaaa"));
w.push(LayoutContainer {
x: 100.0,
y: 50.0,
rows: vec![row(&[AssetId(1), AssetId(2)]), row(&[AssetId(3)])],
..Default::default()
});
apply_label_layout(
&mut w.ctx(),
&loaded_fonts(),
&mut LabelLayoutScratch::default(),
);
assert_eq!(w.label_at(AssetId(1)), (100.0, 48.0));
assert_eq!(w.label_at(AssetId(2)), (126.0, 48.0));
assert_eq!(w.label_at(AssetId(3)), (100.0, 66.0));
}
#[test]
fn apply_label_layout_ignores_hidden_containers() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
w.push(LayoutContainer {
rows: vec![row(&[AssetId(1)])],
visible: false,
..Default::default()
});
apply_label_layout(
&mut w.ctx(),
&loaded_fonts(),
&mut LabelLayoutScratch::default(),
);
assert_eq!(w.label_at(AssetId(1)), (SENTINEL, SENTINEL));
}
#[test]
fn apply_label_layout_drops_labels_it_cannot_measure() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
let mut hidden = chip(AssetId(2), "aaaa");
hidden.visible = false;
w.push(hidden);
let mut orphan = chip(AssetId(3), "aaaa");
orphan.font = Some(FontHandle(99));
w.push(orphan);
w.push(chip(AssetId(4), "aaaaaa"));
w.push(LayoutContainer {
x: 100.0,
y: 50.0,
rows: vec![row(&[AssetId(1), AssetId(2), AssetId(3), AssetId(4)])],
..Default::default()
});
apply_label_layout(
&mut w.ctx(),
&loaded_fonts(),
&mut LabelLayoutScratch::default(),
);
assert_eq!(w.label_at(AssetId(1)), (100.0, 48.0));
assert_eq!(w.label_at(AssetId(4)), (126.0, 48.0));
assert_eq!(w.label_at(AssetId(2)), (SENTINEL, SENTINEL));
assert_eq!(w.label_at(AssetId(3)), (SENTINEL, SENTINEL));
}
#[test]
fn apply_label_layout_reuses_scratch_without_stale_placements() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
w.push(LayoutContainer {
x: 100.0,
y: 50.0,
rows: vec![row(&[AssetId(1)])],
..Default::default()
});
let mut scratch = LabelLayoutScratch::default();
apply_label_layout(&mut w.ctx(), &loaded_fonts(), &mut scratch);
assert_eq!(w.label_at(AssetId(1)), (100.0, 48.0));
for c in w.ctx().query_mut::<LayoutContainer>() {
c.rows.clear();
}
for l in w.ctx().query_mut::<TextLabel>() {
l.x = SENTINEL;
l.y = SENTINEL;
}
apply_label_layout(&mut w.ctx(), &loaded_fonts(), &mut scratch);
assert_eq!(w.label_at(AssetId(1)), (SENTINEL, SENTINEL));
}
#[test]
fn position_debug_hud_right_anchors_chips_top_down() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
w.push(chip(AssetId(2), "aaaa"));
position_debug_hud(
&mut w.ctx(),
&[AssetId(1), AssetId(2)],
&loaded_fonts(),
1000.0,
);
assert_eq!(w.label_at(AssetId(1)), (970.0, 8.0));
assert_eq!(w.label_at(AssetId(2)), (950.0, 26.0));
}
#[test]
fn position_debug_hud_skips_chips_it_cannot_measure() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
w.push(chip(AssetId(2), ""));
let mut orphan = chip(AssetId(3), "aaaa");
orphan.font = Some(FontHandle(99));
w.push(orphan);
w.push(chip(AssetId(4), "aaaa"));
position_debug_hud(
&mut w.ctx(),
&[AssetId(1), AssetId(2), AssetId(3), AssetId(4)],
&loaded_fonts(),
1000.0,
);
assert_eq!(w.label_at(AssetId(4)), (950.0, 26.0));
assert_eq!(w.label_at(AssetId(2)), (SENTINEL, SENTINEL));
assert_eq!(w.label_at(AssetId(3)), (SENTINEL, SENTINEL));
}
#[test]
fn position_debug_hud_clamps_a_chip_wider_than_the_window() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
position_debug_hud(&mut w.ctx(), &[AssetId(1)], &loaded_fonts(), 15.0);
assert_eq!(w.label_at(AssetId(1)).0, 10.0);
}
#[test]
fn position_debug_hud_ignores_an_empty_chip_list_or_window() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
position_debug_hud(&mut w.ctx(), &[], &loaded_fonts(), 1000.0);
assert_eq!(w.label_at(AssetId(1)), (SENTINEL, SENTINEL));
position_debug_hud(&mut w.ctx(), &[AssetId(1)], &loaded_fonts(), 0.0);
assert_eq!(w.label_at(AssetId(1)), (SENTINEL, SENTINEL));
}
#[test]
fn position_stat_hud_packs_chips_left_to_right() {
let mut w = TestWorld::new();
let mut first = chip(AssetId(1), "aa");
first.padding = 4.0;
let mut second = chip(AssetId(2), "aaaa");
second.padding = 4.0;
w.push(first);
w.push(second);
position_stat_hud(&mut w.ctx(), &[AssetId(1), AssetId(2)], &loaded_fonts());
assert_eq!(w.label_at(AssetId(1)), (14.0, 12.0));
assert_eq!(w.label_at(AssetId(2)), (46.0, 12.0));
}
#[test]
fn position_stat_hud_skips_blank_chips() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
w.push(chip(AssetId(2), ""));
w.push(chip(AssetId(3), "aaaa"));
position_stat_hud(
&mut w.ctx(),
&[AssetId(1), AssetId(2), AssetId(3)],
&loaded_fonts(),
);
assert_eq!(w.label_at(AssetId(1)), (10.0, 8.0));
assert_eq!(w.label_at(AssetId(3)), (34.0, 8.0));
assert_eq!(w.label_at(AssetId(2)), (SENTINEL, SENTINEL));
}
#[test]
fn position_stat_hud_ignores_an_empty_chip_list() {
let mut w = TestWorld::new();
w.push(chip(AssetId(1), "aa"));
position_stat_hud(&mut w.ctx(), &[], &loaded_fonts());
assert_eq!(w.label_at(AssetId(1)), (SENTINEL, SENTINEL));
}
}