use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy_ecs::hierarchy::{ChildOf, Children};
use bevy_ecs::prelude::{Changed, Component, Entity, Or, Query, Ref, Res, With, Without};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_ui::{Display, Node};
use toml::Value;
use super::build::FormField;
use super::codec::{get_path, is_within};
use super::domain::{FieldKind, FieldSpec};
use super::editing::{EditSession, Editing};
use super::form::FormTargets;
use super::offers::Offer;
use super::select::Select;
use crate::commands::tui::layout::{NO_STOP, set_display};
#[derive(Component, Debug)]
pub struct Dependent {
key: &'static str,
gate: Option<&'static str>,
}
pub fn gate_of(fields: &[FieldSpec], key: &str) -> Option<&'static str> {
let is_gate =
|gate: &&FieldSpec| matches!(gate.kind, FieldKind::Presence(_)) && is_within(key, gate.key);
fields.iter().find(is_gate).map(|gate| gate.key)
}
impl Dependent {
pub fn of(fields: &[FieldSpec], spec: &FieldSpec) -> Option<Self> {
let gate = gate_of(fields, spec.key);
let key = spec.key;
(gate.is_some() || spec.shown.is_some()).then_some(Self { key, gate })
}
fn is_shown(&self, editing: &Editing) -> bool {
let is_held = |gate| get_path(&editing.snapshot, gate).is_some();
self.gate.is_none_or(is_held) && editing.uses(self.key)
}
}
pub fn place_dependents(
session: Res<EditSession>,
tree: Query<&Children>,
mut rows: Query<(Ref<Dependent>, &mut Node)>,
) {
if !session.is_changed() && !rows.iter().any(|(row, _)| row.is_added()) {
return;
}
let Some((editing, form)) = session.0.as_ref().and_then(|held| Some((held, held.form?))) else {
return;
};
for &row in tree.get(form).into_iter().flatten() {
let Ok((dependent, mut node)) = rows.get_mut(row) else {
continue;
};
set_display(&mut node, dependent.is_shown(editing));
}
}
pub fn place_stops(
moved: Query<(), (Changed<Node>, Or<(With<FormField>, With<Dependent>)>)>,
mut stops: Query<(Entity, &Node, &mut TabIndex), (With<FormField>, Without<Dependent>)>,
parents: Query<&ChildOf>,
rows: Query<&Node, With<Dependent>>,
) {
if moved.is_empty() {
return;
}
let is_hidden = |node: &Node| node.display == Display::None;
for (widget, own, mut tab) in &mut stops {
let mut above = parents.iter_ancestors(widget);
let is_shown = !is_hidden(own) && !above.any(|row| rows.get(row).is_ok_and(is_hidden));
tab.set_if_neq(TabIndex(if is_shown { 0 } else { NO_STOP }));
}
}
pub fn offer_unused(mut selects: Query<(Entity, &FormField, &mut Select)>, targets: FormTargets) {
let is_place = |field: &FormField| matches!(field.spec.kind, FieldKind::Order(..));
let mut places = selects.iter_mut();
if !places.any(|(_, field, select)| is_place(field) && select.is_changed()) {
return;
}
let held: Vec<(Entity, Option<Entity>, &str, Option<Value>)> = selects
.iter()
.filter(|(_, field, _)| is_place(field))
.map(|(entity, field, select)| {
(
entity,
targets.form_of(entity),
field.spec.key,
select.value(),
)
})
.collect();
for (entity, field, mut select) in &mut selects {
let FieldKind::Order(vocabulary, _) = field.spec.kind else {
continue;
};
let form = targets.form_of(entity);
let own = select.value();
let is_taken = |offer: &Offer| {
let word = Some(Value::String(offer.value.clone()));
word != own
&& held.iter().any(|(other, in_form, key, value)| {
*other != entity && *in_form == form && *key == field.spec.key && *value == word
})
};
let unused = vocabulary
.offers()
.into_iter()
.filter(|offer| !is_taken(offer));
Select::fill(&mut select, Some(unused.collect()), own.as_ref());
}
}
const EMPTY_ORDER: &str = "needs at least one of its rows picked";
pub fn ordered(
parts: impl Iterator<Item = (usize, Option<Value>)>,
) -> (Option<Value>, Option<&'static str>) {
let mut held: Vec<(usize, Value)> = parts
.filter_map(|(place, value)| Some((place, value?)))
.collect();
if held.is_empty() {
return (None, Some(EMPTY_ORDER));
}
held.sort_by_key(|(place, _)| *place);
let order = held.into_iter().map(|(_, value)| value).collect();
(Some(Value::Array(order)), None)
}
const GAPPED_LIST: &str = "is blank, but the row after it is not";
pub fn nth(value: Option<&Value>, place: usize) -> Option<&Value> {
value?.as_array()?.get(place)
}
pub fn nth_back(value: Option<&Value>, back: usize) -> Option<&Value> {
let list = value?.as_array()?;
list.get(list.len().checked_sub(back + 1)?)
}
pub fn listed(
parts: impl Iterator<Item = (usize, Option<Value>)>,
) -> (Option<Value>, Option<&'static str>) {
let mut held: Vec<(usize, Value)> = parts
.filter_map(|(back, value)| Some((back, value?)))
.collect();
held.sort_by_key(|(back, _)| *back);
let is_gapped = held.iter().enumerate().any(|(at, (back, _))| at != *back);
if is_gapped {
return (None, Some(GAPPED_LIST));
}
let list: Vec<Value> = held.into_iter().rev().map(|(_, value)| value).collect();
((!list.is_empty()).then_some(Value::Array(list)), None)
}
#[cfg(test)]
mod tests {
use super::*;
fn amounts(list: &[i64]) -> Value {
Value::Array(list.iter().copied().map(Value::Integer).collect())
}
#[test]
fn a_list_is_read_and_made_from_its_end() {
let list = amounts(&[180_000, 185_000]);
assert_eq!(nth_back(Some(&list), 0), Some(&Value::Integer(185_000)));
assert_eq!(nth_back(Some(&list), 1), Some(&Value::Integer(180_000)));
assert_eq!(nth_back(Some(&list), 2), None);
assert_eq!(nth_back(None, 0), None);
let recent = Some(Value::Integer(185_000));
let before = Some(Value::Integer(180_000));
let both = [(0, recent.clone()), (1, before.clone())];
assert_eq!(listed(both.into_iter()), (Some(list), None), "oldest first");
let alone = [(0, recent), (1, None)];
assert_eq!(listed(alone.into_iter()), (Some(amounts(&[185_000])), None));
assert_eq!(listed([(0, None), (1, None)].into_iter()), (None, None));
let gapped = [(0, None), (1, before)];
assert_eq!(listed(gapped.into_iter()), (None, Some(GAPPED_LIST)));
}
}