use nalgebra_glm::Vec2;
use winit::keyboard::KeyCode;
use crate::ecs::world::World;
use super::snapshot_interaction;
pub(super) struct ScrollFrameInputs {
pub entity: freecs::Entity,
pub content_entity: freecs::Entity,
pub thumb_entity: freecs::Entity,
pub track_entity: freecs::Entity,
pub scroll_offset: f32,
pub thumb_dragging: bool,
pub thumb_drag_start_offset: f32,
pub snap_interval: Option<f32>,
pub scroll_delta: Vec2,
pub mouse_position: Vec2,
pub dpi_scale: f32,
}
pub(super) struct ScrollFrameResult {
pub scroll_offset: f32,
pub thumb_dragging: bool,
pub thumb_drag_start_offset: f32,
pub content_height: f32,
pub visible_height: f32,
}
fn is_innermost_scroll_for(
world: &World,
hovered: freecs::Entity,
scroll_entity: freecs::Entity,
) -> bool {
let mut current = hovered;
loop {
if world.ui.get_ui_scroll_area(current).is_some() {
return current == scroll_entity;
}
match world.core.get_parent(current).and_then(|parent| parent.0) {
Some(parent) => current = parent,
None => return false,
}
}
}
pub(super) fn apply_scroll_frame(
world: &mut World,
inputs: ScrollFrameInputs,
) -> ScrollFrameResult {
let visible_height = world
.ui
.get_ui_layout_node(inputs.entity)
.map(|node| node.computed_rect.height())
.unwrap_or(0.0);
let children: &[freecs::Entity] = world
.resources
.transform_state
.children_cache
.get(&inputs.content_entity)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let content_flow = world
.ui
.get_ui_layout_node(inputs.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 * inputs.dpi_scale * (visible_count - 1) as f32;
}
total_content_height += flow.padding * inputs.dpi_scale * 2.0;
}
let max_scroll = (total_content_height - visible_height).max(0.0) / inputs.dpi_scale;
let mut scroll_offset = inputs.scroll_offset;
let mut thumb_dragging = inputs.thumb_dragging;
let mut thumb_drag_start_offset = inputs.thumb_drag_start_offset;
let scroll_under_cursor = world
.resources
.retained_ui
.interaction
.hovered_entity
.is_some_and(|hovered| is_innermost_scroll_for(world, hovered, inputs.entity));
if scroll_under_cursor && inputs.scroll_delta.y.abs() > 0.0 {
scroll_offset -= inputs.scroll_delta.y * 40.0;
scroll_offset = scroll_offset.clamp(0.0, max_scroll);
}
let thumb_interaction = snapshot_interaction(world, inputs.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(inputs.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 = inputs.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) = inputs.snap_interval
&& snap > 0.0
&& !thumb_dragging
&& inputs.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.timing.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(inputs.content_entity) {
content_node.scroll_offset = Vec2::new(0.0, scroll_offset);
}
let show_scrollbar = total_content_height > visible_height;
if let Some(track_node) = world.ui.get_ui_layout_node_mut(inputs.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(inputs.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 * inputs.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 / inputs.dpi_scale;
let thumb_height = thumb_height_physical / inputs.dpi_scale;
if let Some(thumb_node) = world.ui.get_ui_layout_node_mut(inputs.thumb_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
thumb_node.base_layout.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();
}
}
}
ScrollFrameResult {
scroll_offset,
thumb_dragging,
thumb_drag_start_offset,
content_height: total_content_height,
visible_height,
}
}
pub(super) fn handle_scroll_area(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiScrollAreaData,
scroll_delta: Vec2,
mouse_position: Vec2,
dpi_scale: f32,
) {
let result = apply_scroll_frame(
world,
ScrollFrameInputs {
entity,
content_entity: data.content_entity,
thumb_entity: data.thumb_entity,
track_entity: data.track_entity,
scroll_offset: data.scroll_offset,
thumb_dragging: data.thumb_dragging,
thumb_drag_start_offset: data.thumb_drag_start_offset,
snap_interval: data.snap_interval,
scroll_delta,
mouse_position,
dpi_scale,
},
);
if let Some(widget_data) = world.ui.get_ui_scroll_area_mut(entity) {
widget_data.scroll_offset = result.scroll_offset;
widget_data.content_height = result.content_height;
widget_data.visible_height = result.visible_height;
widget_data.thumb_dragging = result.thumb_dragging;
widget_data.thumb_drag_start_offset = result.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(scroll_data) = world.ui.get_ui_scroll_area(data.scroll_entity) {
scroll_data.scroll_offset
} else {
0.0
};
let window = super::pool_window::compute(
scroll_offset,
data.item_height,
data.total_items,
data.pool_size,
);
let visible_start = window.visible_start;
let top_height = window.top_height;
let bottom_height = window.bottom_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.events_for_active_mut().push(
crate::ecs::ui::resources::UiEvent::VirtualListItemClicked {
entity,
item_index,
},
);
}
if interaction.right_clicked {
let screen_position = world.resources.input.mouse.position;
world.resources.retained_ui.events_for_active_mut().push(
crate::ecs::ui::resources::UiEvent::VirtualListItemRightClicked {
entity,
item_index,
screen_position,
},
);
}
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(widget_data) = world.ui.get_ui_virtual_list_mut(entity) {
widget_data.visible_start = visible_start;
widget_data.selection = selection;
widget_data.selection_changed = selection_changed;
}
}