use concinnity_core::ecs::World;
use crate::editor::theme;
use crate::editor::widget::{self, place_rounded, point_in};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ExtraControl {
Label,
Check {
on: bool,
enabled: bool,
},
Choice {
options: Vec<String>,
selected: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExtraRow {
pub(crate) id: usize,
pub(crate) caption: String,
pub(crate) indent: bool,
pub(crate) control: ExtraControl,
pub(crate) detail: Option<String>,
}
impl ExtraRow {
pub(crate) fn label(caption: impl Into<String>, detail: Option<String>) -> Self {
Self {
id: 0,
caption: caption.into(),
indent: false,
control: ExtraControl::Label,
detail,
}
}
pub(crate) fn indented(mut self) -> Self {
self.indent = true;
self
}
pub(crate) fn pressable(&self) -> bool {
match &self.control {
ExtraControl::Label => false,
ExtraControl::Check { enabled, .. } => *enabled,
ExtraControl::Choice { options, .. } => options.len() > 1,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Slot {
pub(crate) row: [f32; 4],
pub(crate) control: [f32; 4],
pub(crate) toggle: [f32; 4],
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SlotIds {
pub(crate) caption: concinnity_core::ecs::asset_id::AssetId,
pub(crate) control: concinnity_core::ecs::asset_id::AssetId,
pub(crate) text: concinnity_core::ecs::asset_id::AssetId,
}
const PAD: f32 = 10.0;
const INDENT: f32 = 14.0;
const LABEL: [f32; 3] = [0.90, 0.90, 0.92];
const LABEL_DIM: [f32; 3] = [0.60, 0.60, 0.66];
const CHECK_ON: [f32; 4] = [0.30, 0.66, 0.34, 1.0];
const CHECK_OFF: [f32; 4] = [0.30, 0.30, 0.34, 1.0];
const CHECK_ON_DISABLED: [f32; 4] = [0.24, 0.40, 0.27, 1.0];
const CHECK_OFF_DISABLED: [f32; 4] = [0.22, 0.22, 0.25, 1.0];
pub(crate) fn hits(row: &ExtraRow, slot: &Slot, mx: f32, my: f32) -> bool {
row.pressable() && point_in(mx, my, slot.control)
}
pub(crate) fn place(world: &mut World, row: &ExtraRow, slot: &Slot, ids: SlotIds, mouse: [f32; 2]) {
let indent = if row.indent { INDENT } else { 0.0 };
let note = row.control == ExtraControl::Label && row.indent;
widget::place_left_label(
world,
ids.caption,
[
slot.row[0] + PAD + indent,
slot.row[1] + slot.row[3] * 0.5 - theme::TEXT_HALF,
],
&row.caption,
if note { LABEL_DIM } else { LABEL },
true,
);
let text_x = match &row.control {
ExtraControl::Label => slot.control[0],
ExtraControl::Check { on, enabled } => {
let tint = match (on, enabled) {
(true, true) => CHECK_ON,
(false, true) => CHECK_OFF,
(true, false) => CHECK_ON_DISABLED,
(false, false) => CHECK_OFF_DISABLED,
};
place_rounded(world, ids.control, slot.toggle, tint, 4.0, true);
slot.toggle[0] + slot.toggle[2] + 8.0
}
ExtraControl::Choice { options, selected } => {
let c = slot.control;
let hover = row.pressable() && point_in(mouse[0], mouse[1], c);
let tint = if hover {
theme::HOVER_TINT
} else {
theme::BUTTON_TINT
};
place_rounded(world, ids.control, c, tint, theme::CONTROL_RADIUS, true);
let value = options.get(*selected).map(String::as_str).unwrap_or("");
widget::place_left_label(
world,
ids.text,
[c[0] + 8.0, c[1] + c[3] * 0.5 - theme::TEXT_HALF],
value,
LABEL,
true,
);
return;
}
};
if let Some(detail) = &row.detail {
widget::place_left_label(
world,
ids.text,
[
text_x,
slot.control[1] + slot.control[3] * 0.5 - theme::TEXT_HALF,
],
detail,
LABEL_DIM,
true,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::components::{Sprite, TextLabel};
use concinnity_core::ecs::asset_id::AssetId;
fn check(on: bool, enabled: bool) -> ExtraRow {
ExtraRow {
id: 7,
caption: "vertex".to_string(),
indent: true,
control: ExtraControl::Check { on, enabled },
detail: Some("a.hlsl".to_string()),
}
}
fn slot() -> Slot {
Slot {
row: [0.0, 100.0, 400.0, 34.0],
control: [190.0, 103.0, 200.0, 26.0],
toggle: [190.0, 103.0, 22.0, 22.0],
}
}
const IDS: SlotIds = SlotIds {
caption: AssetId(1),
control: AssetId(2),
text: AssetId(3),
};
#[test]
fn only_an_enabled_check_or_a_real_choice_is_pressable() {
assert!(check(false, true).pressable());
assert!(!check(true, false).pressable());
assert!(!ExtraRow::label("Stages", None).pressable());
let choice = |n: usize| ExtraRow {
control: ExtraControl::Choice {
options: (0..n).map(|i| i.to_string()).collect(),
selected: 0,
},
..check(false, true)
};
assert!(choice(2).pressable() && !choice(1).pressable());
}
#[test]
fn a_press_lands_on_the_control_column_only() {
let row = check(false, true);
assert!(hits(&row, &slot(), 300.0, 110.0));
assert!(!hits(&row, &slot(), 20.0, 110.0), "the caption column");
assert!(!hits(&check(false, false), &slot(), 300.0, 110.0));
}
#[test]
fn a_check_draws_its_box_and_its_detail_after_it() {
let mut world = World::new();
world.push_identified(IDS.caption, TextLabel::default());
world.push_identified(IDS.control, Sprite::default());
world.push_identified(IDS.text, TextLabel::default());
place(&mut world, &check(true, false), &slot(), IDS, [0.0, 0.0]);
let sprite = world.get_by_id::<Sprite>(IDS.control).unwrap();
assert!(sprite.visible);
assert_eq!(sprite.tint, CHECK_ON_DISABLED);
let text = world.get_by_id::<TextLabel>(IDS.text).unwrap();
assert_eq!(text.content, "a.hlsl");
assert!(text.x > slot().toggle[0] + slot().toggle[2]);
let caption = world.get_by_id::<TextLabel>(IDS.caption).unwrap();
assert_eq!(caption.content, "vertex");
}
}