use crate::{
ActiveMasksSecondary, CapacityConfig, ChildrenSecondary, ClipRectsSecondary, DEFAULT_FLEX,
DebugStore, DirtyLayoutEntitiesVec, DirtyRenderEntitiesVec, EntityId, InputContentsSparse,
LayoutPoint, LayoutSize, LayoutStore, MichiuSoA, OutputStore, ParentsSecondary, Position,
RectsSecondary, RenderStore, ResolvedBasicSecondary, ResolvedFlexSecondary, ResolvedGeometry,
ScrollbarStylesSparse, SystemStore, TaffyNodesSecondary, TaffyTreeEntityId, TextBufferSparse,
TextContentsSparse, TextEngine, TextSpansSparse, UserSelect, Val, VisualPropertiesSecondary,
WindowStore, define_secondary,
};
use slotmap::SecondaryMap;
use std::time::Instant;
define_secondary!(pub struct ScrollOffsetsSecondary(LayoutPoint));
define_secondary!(pub struct ScrollSizesSecondary(LayoutSize));
pub(crate) struct ScrollStore {
pub(crate) sc_offsets: ScrollOffsetsSecondary,
pub(crate) sc_sizes: ScrollSizesSecondary,
}
impl Default for ScrollStore {
fn default() -> Self {
Self::new()
}
}
impl ScrollStore {
#[inline]
#[must_use]
pub(crate) fn new() -> Self {
Self {
sc_offsets: ScrollOffsetsSecondary(SecondaryMap::new()),
sc_sizes: ScrollSizesSecondary(SecondaryMap::new()),
}
}
#[inline]
#[must_use]
pub(crate) fn with_capacity(c: &CapacityConfig) -> Self {
Self {
sc_offsets: ScrollOffsetsSecondary(SecondaryMap::with_capacity(c.sc_offsets)),
sc_sizes: ScrollSizesSecondary(SecondaryMap::with_capacity(c.sc_sizes)),
}
}
#[inline]
pub(crate) fn clear(&mut self) {
self.sc_offsets.clear();
self.sc_sizes.clear();
}
#[inline]
pub(crate) fn despawn(&mut self, id: EntityId) {
self.sc_offsets.remove(id);
self.sc_sizes.remove(id);
}
}
impl ScrollStore {
#[track_caller]
pub(crate) fn scroll_to(
id: EntityId,
mut x: f32,
mut y: f32,
win_last_size: Option<LayoutSize>,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) -> bool {
let rect = out_rects.find_or_default(id, debug);
let scroll_size = sc_sizes.find_or_default(id, debug);
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
let visible_size = WindowStore::calc_visible_size(rect, win_last_size);
let content_size = resolved_geom.inner_content_size(visible_size);
let max_scroll_x = (scroll_size.width - content_size.width).max(0.0);
let max_scroll_y = (scroll_size.height - content_size.height).max(0.0);
x = x.clamp(0.0, max_scroll_x);
y = y.clamp(0.0, max_scroll_y);
if !sc_offsets.contains_key(id) {
sc_offsets.insert(id, LayoutPoint::ZERO);
}
let current = sc_offsets.at_mut(id);
if (current.x - x).abs() > 0.01 || (current.y - y).abs() > 0.01 {
current.x = x;
current.y = y;
if let Some(sb_state) = bar_styles.find_mut(id) {
sb_state.last_scroll_time = Some(Instant::now());
}
LayoutStore::mark_layout_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
debug,
);
true
} else {
false
}
}
#[track_caller]
pub(crate) fn sync_scrollbar_drag(
logical_pos: LayoutPoint,
win_last_size: Option<LayoutSize>,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
sc_offsets: &mut ScrollOffsetsSecondary,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) {
#[derive(Clone, Copy, PartialEq, Eq)]
enum DragDirection {
Vertical,
Horizontal,
}
#[derive(Clone, Copy, PartialEq)]
struct SrcrollbarDate {
track_len: f32,
thumb_len: f32,
margin_start: f32,
margin_end: f32,
delta_mouse: f32,
max_scroll_len: f32,
start_scroll_offset: f32,
}
let active_drag_target = bar_styles.iter().find_map(|(id, state)| {
if state.v_thumb_dragged {
Some((id, DragDirection::Vertical))
} else if state.h_thumb_dragged {
Some((id, DragDirection::Horizontal))
} else {
None
}
});
let Some((current_id, direction)) = active_drag_target else {
return;
};
let (sb_state, container_rect, scroll_size) = {
let sb_state = bar_styles.find_or_default(current_id, debug);
let container_rect = out_rects.find_or_default(current_id, debug);
let scroll_size = sc_sizes.find_or_default(current_id, debug);
(sb_state, container_rect, scroll_size)
};
let visible_size = WindowStore::calc_visible_size(container_rect, win_last_size);
let date = match direction {
DragDirection::Vertical => {
let (Some(track_id), Some(thumb_id)) = (sb_state.v_track_id, sb_state.v_thumb_id)
else {
return;
};
let track_rect = out_rects.find_or_default(track_id, debug);
let thumb_rect = out_rects.find_or_default(thumb_id, debug);
let (mut margin_top, mut margin_bottom) = (0.0, 0.0);
if let Some(ref thumb_style) = sb_state.style.v_thumb {
if let Val::Px(val) = thumb_style.inner.basic_layout.margin.top {
margin_top = val;
}
if let Val::Px(val) = thumb_style.inner.basic_layout.margin.bottom {
margin_bottom = val;
}
}
SrcrollbarDate {
track_len: track_rect.height,
thumb_len: thumb_rect.height,
margin_start: margin_top,
margin_end: margin_bottom,
delta_mouse: logical_pos.y - sb_state.drag_start_mouse.y,
max_scroll_len: scroll_size.height - visible_size.height,
start_scroll_offset: sb_state.drag_start_offset.y,
}
}
DragDirection::Horizontal => {
let (Some(track_id), Some(thumb_id)) = (sb_state.h_track_id, sb_state.h_thumb_id)
else {
return;
};
let track_rect = *out_rects.at(track_id);
let thumb_rect = *out_rects.at(thumb_id);
let (mut margin_left, mut margin_right) = (0.0, 0.0);
if let Some(ref thumb_style) = sb_state.style.h_thumb {
if let Val::Px(val) = thumb_style.inner.basic_layout.margin.left {
margin_left = val;
}
if let Val::Px(val) = thumb_style.inner.basic_layout.margin.right {
margin_right = val;
}
}
SrcrollbarDate {
track_len: track_rect.width,
thumb_len: thumb_rect.width,
margin_start: margin_left,
margin_end: margin_right,
delta_mouse: logical_pos.x - sb_state.drag_start_mouse.x,
max_scroll_len: scroll_size.width - visible_size.width,
start_scroll_offset: sb_state.drag_start_offset.x,
}
}
};
let track_range = date.track_len - date.thumb_len - date.margin_start - date.margin_end;
if track_range > 0.0 {
let ratio = date.max_scroll_len / track_range;
let target_scroll = date.start_scroll_offset + date.delta_mouse * ratio;
let (target_x, target_y) = match direction {
DragDirection::Vertical => {
let current_x = sc_offsets.find(current_id).map_or(0.0, |o| o.x);
(current_x, target_scroll)
}
DragDirection::Horizontal => {
let current_y = sc_offsets.find(current_id).map_or(0.0, |o| o.y);
(target_scroll, current_y)
}
};
ScrollStore::scroll_to(
current_id,
target_x,
target_y,
win_last_size,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
sc_offsets,
out_rects,
sc_sizes,
debug,
);
}
RenderStore::mark_render_dirty(current_id, topo_active_masks, rnd_dirty_entities);
}
pub(crate) fn scroll_by(
id: EntityId,
dx: f32,
dy: f32,
win_last_size: Option<LayoutSize>,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) -> bool {
let current = sc_offsets.find_or_default(id, debug);
ScrollStore::scroll_to(
id,
current.x + dx,
current.y + dy,
win_last_size,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
sc_offsets,
out_rects,
sc_sizes,
debug,
)
}
#[track_caller]
pub(crate) fn autoscroll_occurred(
id: EntityId,
win_last_size: Option<LayoutSize>,
evt_current_pointer_position: Option<LayoutPoint>,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
rnd_visual: &VisualPropertiesSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
out_rects: &RectsSecondary,
out_clip_rects: &ClipRectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) -> (bool, Option<LayoutPoint>) {
let Some(pointer_pos) = evt_current_pointer_position else {
return (false, None);
};
let clip = *out_clip_rects.at(id);
let user_select = rnd_visual.user_select(id);
if user_select != UserSelect::Text {
return (false, None);
}
let distance = OutputStore::drag_overhang_distance(pointer_pos, clip);
if distance.x.abs() <= 1.0 && distance.y.abs() <= 1.0 {
return (false, None);
}
let speed_factor = 0.15f32;
let dx = distance.x * speed_factor;
let dy = distance.y * speed_factor;
let scroll = ScrollStore::scroll_by(
id,
dx,
dy,
win_last_size,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
sc_offsets,
out_rects,
sc_sizes,
debug,
);
if scroll {
(true, Some(pointer_pos))
} else {
(false, None)
}
}
#[track_caller]
pub(crate) fn get_scroll_size(
id: EntityId,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &TextContentsSparse,
cont_text_spans: &TextSpansSparse,
cont_input_contents: &InputContentsSparse,
topo_active_masks: &ActiveMasksSecondary,
topo_children: &ChildrenSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
bar_styles: &ScrollbarStylesSparse,
rnd_visual: &VisualPropertiesSecondary,
out_rects: &RectsSecondary,
sc_offsets: &ScrollOffsetsSecondary,
debug: &mut DebugStore,
) -> LayoutSize {
let mut max_x = 0.0f32;
let mut max_y = 0.0f32;
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
if topo_active_masks.at(id).has_input_content() {
let contents = cont_input_contents.at(id);
if let Some(layout_rect) = contents.last_layout {
max_x = layout_rect.width
+ contents.caret_width.unwrap_or(contents.default_caret_width);
max_y = layout_rect.height;
}
} else if topo_active_masks.at(id).has_text_content() {
let auto_wrap = rnd_visual.auto_wrap(id);
let max_width_opt = resolved_geom.calc_max_width(auto_wrap);
let buffer =
SystemStore::get_or_create_text_buffer(id, max_width_opt, sys_text_buffers, || {
let text = cont_text_contents.at(id);
let font = rnd_visual.font(id);
let spans = cont_text_spans.span(id);
let flex = lay_resolved_flex.find_or(id, &DEFAULT_FLEX, debug);
sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
});
let size = TextEngine::get_layout_size(&buffer);
max_x = size.width;
max_y = size.height;
}
let offset_x = resolved_geom.border.left + resolved_geom.padding.left;
let offset_y = resolved_geom.border.top + resolved_geom.padding.top;
let (v_track_opt, h_track_opt) = if let Some(sb_state) = bar_styles.find(id) {
(sb_state.v_track_id, sb_state.h_track_id)
} else {
(None, None)
};
let children_list = topo_children.at(id);
for &child_id in children_list {
if Some(child_id) == v_track_opt || Some(child_id) == h_track_opt {
continue;
}
let is_absolute = lay_resolved_basic
.find(child_id)
.is_some_and(|l| l.position == Position::Absolute);
if is_absolute {
continue;
}
let child_rect = *out_rects.at(child_id);
let parent_rect = *out_rects.at(id);
let scroll_offset = sc_offsets.find_or_default(id, debug);
let local_right =
child_rect.x - parent_rect.x + scroll_offset.x + child_rect.width - offset_x;
let local_bottom =
child_rect.y - parent_rect.y + scroll_offset.y + child_rect.height - offset_y;
max_x = max_x.max(local_right);
max_y = max_y.max(local_bottom);
}
LayoutSize::new(max_x, max_y)
}
}