use bevy::input_focus::{FocusCause, InputFocus, InputFocusVisible};
use bevy::prelude::*;
use bevy::ui::auto_directional_navigation::AutoDirectionalNavigation;
use bevy::ui::{ComputedNode, ScrollPosition, UiGlobalTransform};
use bevy::ui_widgets::Scrollbar;
use crate::model::{page_target, scroll_to_reveal, snap_to_row};
use crate::nav::show_focus;
use crate::skin::Role;
const CONTEXT_ROWS: f32 = 1.5;
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct ListView {
pub rows: f32,
}
pub fn update_lists(
mut rails: Query<(&Role, &Scrollbar, &mut Node)>,
views: Query<&ComputedNode, With<ListView>>,
) {
for (role, bar, mut node) in &mut rails {
if *role != Role::ListRail {
continue;
}
let Ok(view) = views.get(bar.target) else {
continue;
};
let shown = view.content_size().y > view.size().y - view.scrollbar_size.y + 0.5;
let want = if shown { Display::Flex } else { Display::None };
if node.display != want {
node.display = want;
}
}
}
pub(crate) fn span(node: &ComputedNode, transform: &UiGlobalTransform) -> (f32, f32) {
let scale = node.inverse_scale_factor();
let height = node.size().y * scale;
(
transform.affine().translation.y * scale - height * 0.5,
height,
)
}
fn list_of(
entity: Entity,
parents: &Query<&ChildOf>,
views: &Query<(), With<ListView>>,
) -> Option<Entity> {
parents.iter_ancestors(entity).find(|&a| views.contains(a))
}
pub fn scroll_to_focus(
focus: Res<InputFocus>,
visible: Res<InputFocusVisible>,
parents: Query<&ChildOf>,
lists: Query<(), With<ListView>>,
nodes: Query<(&ComputedNode, &UiGlobalTransform), Without<ListView>>,
columns: Query<(&Node, &Children)>,
mut views: Query<
(
&ComputedNode,
&UiGlobalTransform,
&Children,
&mut ScrollPosition,
),
With<ListView>,
>,
) {
if !visible.0 || !(focus.is_changed() || visible.is_changed()) {
return;
}
let Some(target) = focus.get() else {
return;
};
let Some(list) = list_of(target, &parents, &lists) else {
return;
};
let (Ok((node, transform)), Ok((view, view_transform, view_children, mut scroll))) =
(nodes.get(target), views.get_mut(list))
else {
return;
};
let scale = view.inverse_scale_factor();
let (top, height) = span(node, transform);
let (view_top, view_height) = span(view, view_transform);
let laid_out = view.scroll_position.y * scale;
let max = (view.content_size().y + view.scrollbar_size.y - view.size().y).max(0.0) * scale;
let from_start = top - view_top + laid_out;
let column = view_children
.first()
.and_then(|&column| columns.get(column).ok());
let gap = column.map_or(0.0, |(column, _)| match column.row_gap {
Val::Px(gap) => gap,
_ => 0.0,
});
let want = scroll_to_reveal(
from_start,
from_start + height,
view_height,
laid_out,
max,
CONTEXT_ROWS * (height + gap),
);
let tops: Vec<f32> = column
.into_iter()
.flat_map(|(_, rows)| rows.iter())
.filter_map(|row| nodes.get(row).ok())
.map(|(node, transform)| span(node, transform).0 - view_top + laid_out)
.collect();
let want = snap_to_row(want, laid_out, &tops)
.filter(|&snapped| {
want < max - 0.5
&& snapped <= max
&& from_start >= snapped - 0.5
&& from_start + height <= snapped + view_height + 0.5
})
.unwrap_or(want);
if (want - scroll.y).abs() > 0.5 {
scroll.y = want;
}
}
type PageNode = (
Entity,
&'static ComputedNode,
&'static UiGlobalTransform,
&'static InheritedVisibility,
Has<AutoDirectionalNavigation>,
Has<ListView>,
);
pub fn page_lists(
keys: Res<ButtonInput<KeyCode>>,
gamepads: Query<&Gamepad>,
mut focus: ResMut<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
parents: Query<&ChildOf>,
lists: Query<(), With<ListView>>,
nodes: Query<PageNode>,
) {
let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
let pressed = |key: KeyCode, pad: GamepadButton| {
(!ctrl && keys.just_pressed(key)) || gamepads.iter().any(|g| g.just_pressed(pad))
};
let down = pressed(KeyCode::PageDown, GamepadButton::RightTrigger2);
let up = pressed(KeyCode::PageUp, GamepadButton::LeftTrigger2);
if down == up {
return;
}
let Some(from) = focus.get() else {
return;
};
let Some(list) = list_of(from, &parents, &lists) else {
return;
};
let (Ok((_, from_node, from_transform, ..)), Ok((_, view, view_transform, ..))) =
(nodes.get(from), nodes.get(list))
else {
return;
};
show_focus(&mut shown);
let (from_top, from_height) = span(from_node, from_transform);
let (_, view_height) = span(view, view_transform);
let page = (view_height - from_height).max(from_height);
let candidates: Vec<(Entity, f32)> = nodes
.iter()
.filter(|&(entity, node, _, visible, navigable, is_list)| {
navigable
&& !is_list
&& !node.is_empty()
&& visible.get()
&& list_of(entity, &parents, &lists) == Some(list)
})
.map(|(entity, node, transform, ..)| {
let (top, height) = span(node, transform);
(entity, top + height * 0.5)
})
.collect();
let centres: Vec<f32> = candidates.iter().map(|&(_, centre)| centre).collect();
let signed = if down { page } else { -page };
if let Some(to) = page_target(from_top + from_height * 0.5, ¢res, signed) {
focus.set(candidates[to].0, FocusCause::Navigated);
}
}