use crate::ecs::ui::state::UiStateTrait as _;
use nalgebra_glm::Vec2;
use winit::keyboard::KeyCode;
use crate::ecs::ui::components::UiWidgetState;
use crate::ecs::world::World;
use super::InteractionSnapshot;
use super::snapshot_interaction;
pub(super) fn handle_scroll_area(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiScrollAreaData,
scroll_delta: Vec2,
mouse_position: Vec2,
dpi_scale: f32,
) {
let visible_height = world
.ui
.get_ui_layout_node(entity)
.map(|node| node.computed_rect.height())
.unwrap_or(0.0);
let children: Vec<freecs::Entity> = world
.resources
.children_cache
.get(&data.content_entity)
.map(|v| v.to_vec())
.unwrap_or_default();
let content_flow = world
.ui
.get_ui_layout_node(data.content_entity)
.and_then(|node| node.flow_layout);
let mut total_content_height = 0.0f32;
let mut visible_count = 0usize;
for child in &children {
if let Some(node) = world.ui.get_ui_layout_node(*child)
&& node.visible
{
total_content_height += node.computed_rect.height();
visible_count += 1;
}
}
if let Some(flow) = content_flow {
if visible_count > 1 {
total_content_height += flow.spacing * dpi_scale * (visible_count - 1) as f32;
}
total_content_height += flow.padding * dpi_scale * 2.0;
}
let max_scroll = (total_content_height - visible_height).max(0.0) / dpi_scale;
let mut scroll_offset = data.scroll_offset;
let mut thumb_dragging = data.thumb_dragging;
let mut thumb_drag_start_offset = data.thumb_drag_start_offset;
if interaction.hovered && scroll_delta.y.abs() > 0.0 {
scroll_offset -= scroll_delta.y * 40.0;
scroll_offset = scroll_offset.clamp(0.0, max_scroll);
}
let thumb_interaction = snapshot_interaction(world, data.thumb_entity);
if thumb_interaction.pressed && !thumb_dragging {
thumb_dragging = true;
thumb_drag_start_offset = scroll_offset;
}
if thumb_dragging
&& thumb_interaction.pressed
&& let Some(drag_start) = thumb_interaction.drag_start
{
let track_rect = world
.ui
.get_ui_layout_node(data.track_entity)
.map(|n| n.computed_rect);
if let Some(track) = track_rect {
let track_height = track.height();
let thumb_ratio = if total_content_height > 0.0 {
visible_height / total_content_height
} else {
1.0
};
let scrollable_track = track_height * (1.0 - thumb_ratio);
if scrollable_track > 0.0 {
let mouse_delta = mouse_position.y - drag_start.y;
let scroll_per_pixel = max_scroll / scrollable_track;
scroll_offset = (thumb_drag_start_offset + mouse_delta * scroll_per_pixel)
.clamp(0.0, max_scroll);
}
}
}
if !thumb_interaction.pressed {
thumb_dragging = false;
}
if let Some(snap) = data.snap_interval
&& snap > 0.0
&& !thumb_dragging
&& scroll_delta.y.abs() < f32::EPSILON
{
let target = (scroll_offset / snap).round() * snap;
let target = target.clamp(0.0, max_scroll);
let delta_time = world.resources.retained_ui.delta_time;
let diff = target - scroll_offset;
if diff.abs() > 0.1 {
scroll_offset += diff * (10.0 * delta_time).min(1.0);
} else {
scroll_offset = target;
}
}
if let Some(content_node) = world.ui.get_ui_layout_node_mut(data.content_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
content_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
boundary.position_1 = crate::ecs::ui::units::Ab(Vec2::new(0.0, -scroll_offset)).into();
}
let show_scrollbar = total_content_height > visible_height;
if let Some(track_node) = world.ui.get_ui_layout_node_mut(data.track_entity) {
track_node.visible = show_scrollbar;
}
if show_scrollbar {
let thumb_ratio = (visible_height / total_content_height).clamp(0.05, 1.0);
let track_rect = world
.ui
.get_ui_layout_node(data.track_entity)
.map(|n| n.computed_rect);
if let Some(track) = track_rect {
let track_height = track.height();
let thumb_height_physical = (track_height * thumb_ratio).max(20.0 * dpi_scale);
let scrollable_track = track_height - thumb_height_physical;
let scroll_ratio = if max_scroll > 0.0 {
scroll_offset / max_scroll
} else {
0.0
};
let thumb_y = scroll_ratio * scrollable_track / dpi_scale;
let thumb_height = thumb_height_physical / dpi_scale;
if let Some(thumb_node) = world.ui.get_ui_layout_node_mut(data.thumb_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
thumb_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position = crate::ecs::ui::units::Ab(Vec2::new(0.0, thumb_y)).into();
window.size = crate::ecs::ui::units::Ab(Vec2::new(8.0, thumb_height)).into();
}
}
}
if let Some(UiWidgetState::ScrollArea(widget_data)) = world.ui.get_ui_widget_state_mut(entity) {
widget_data.scroll_offset = scroll_offset;
widget_data.content_height = total_content_height;
widget_data.visible_height = visible_height;
widget_data.thumb_dragging = thumb_dragging;
widget_data.thumb_drag_start_offset = thumb_drag_start_offset;
}
}
pub(super) fn handle_virtual_list(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiVirtualListData,
dpi_scale: f32,
frame_keys: &[(KeyCode, bool)],
focused_entity: Option<freecs::Entity>,
) {
let scroll_offset = if let Some(UiWidgetState::ScrollArea(scroll_data)) =
world.ui.get_ui_widget_state(data.scroll_entity)
{
scroll_data.scroll_offset
} else {
0.0
};
let visible_start = if data.item_height > 0.0 {
(scroll_offset / data.item_height).floor() as usize
} else {
0
};
let visible_start = visible_start.min(data.total_items.saturating_sub(data.pool_size));
let visible_end = data.total_items.min(visible_start + data.pool_size);
let top_height = visible_start as f32 * data.item_height;
let bottom_height = data.total_items.saturating_sub(visible_end) as f32 * data.item_height;
if let Some(top_node) = world.ui.get_ui_layout_node_mut(data.top_spacer) {
top_node.flow_child_size = Some(
crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, top_height)),
);
}
if let Some(bottom_node) = world.ui.get_ui_layout_node_mut(data.bottom_spacer) {
bottom_node.flow_child_size = Some(
crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, bottom_height)),
);
}
let mut selection = data.selection;
let mut selection_changed = false;
for (pool_index, item) in data.pool_items.iter().enumerate() {
let item_index = visible_start + pool_index;
let is_visible = item_index < data.total_items;
if let Some(node) = world.ui.get_ui_layout_node_mut(item.container_entity) {
node.visible = is_visible;
}
if is_visible {
let interaction = snapshot_interaction(world, item.container_entity);
if interaction.clicked {
selection = Some(item_index);
selection_changed = true;
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::VirtualListItemClicked {
entity,
item_index,
},
);
}
let accent_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let is_selected = selection == Some(item_index);
if let Some(crate::ecs::ui::components::UiNodeContent::Rect {
border_width,
border_color,
..
}) = world.ui.get_ui_node_content_mut(item.container_entity)
{
if is_selected {
*border_width = 1.0 / dpi_scale;
*border_color = accent_color;
} else {
*border_width = 0.0;
}
}
}
}
let is_focused = focused_entity == Some(entity)
|| data
.pool_items
.iter()
.any(|item| focused_entity == Some(item.container_entity));
if is_focused && data.total_items > 0 {
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::ArrowUp => {
let current = selection.unwrap_or(0);
if current > 0 {
selection = Some(current - 1);
selection_changed = true;
}
}
KeyCode::ArrowDown => {
let current = selection.map_or(0, |s| s + 1);
if current < data.total_items {
selection = Some(current);
selection_changed = true;
}
}
KeyCode::Home => {
selection = Some(0);
selection_changed = true;
}
KeyCode::End => {
selection = Some(data.total_items - 1);
selection_changed = true;
}
_ => {}
}
}
}
if let Some(UiWidgetState::VirtualList(widget_data)) = world.ui.get_ui_widget_state_mut(entity)
{
widget_data.visible_start = visible_start;
widget_data.selection = selection;
widget_data.selection_changed = selection_changed;
}
}